Skip to content
Home » Basics of .NET MAUI – Part 9 – Content Pages and Content Views

Basics of .NET MAUI – Part 9 – Content Pages and Content Views

Spread the love

In the previous part of the series we were talking about the Slugrace app, which is the app we’re going to create. In this part we’ll create the pages that we need for the 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.

The source code for this article is available on Github.

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 part of the series 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 series. In particular, we’re going to create the following pages in this part:

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. We’re not going to create the SplashScreenPage for now either.

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:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Slugrace.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Slugrace"
    Shell.FlyoutBehavior="Disabled">

    <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.

Anyway, 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).

The new page, like any new page you add to the project, contains some initial code:

<?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.SettingsPage"
             Title="SettingsPage">
    <VerticalStackLayout>
        <Label 
            Text="Welcome to .NET MAUI!"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>

In order to test the page, let’s change the page settings in AppShell.xaml:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Slugrace.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Slugrace.Views"
    Shell.FlyoutBehavior="Disabled">

    <ShellContent
        Title=""
        ContentTemplate="{DataTemplate local:SettingsPage}"
        Route="SettingsPage" />
</Shell>

So, first of all we have to set the local namespace to Slugrace.Views, because the page is in the Views folder. Then we have to change the DataTemplate. 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 take care of 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:

And this is what it’s eventually going to look like, more or less:

This page consists of several parts. These parts are visually separated from one another. They include:

– the label with the Text property set to Settings,

– the Players panel,

– the Ending Conditions panel,

– the Ready button.

The first thing we can do is remove the blue 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:

<?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.SettingsPage">             
    <VerticalStackLayout>
        ...

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 Settings label will be implemented as a simple Label.

– 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.

– 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.

Here’s the code in the SettinsPage.xaml file:

<?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.SettingsPage"
             Title="">
    <VerticalStackLayout
        Margin="10">
        
        <!--the Settings label-->
        <Label 
            Text="Settings"
            VerticalOptions="Center" 
            FontSize="18" />
        
        <!--the Players panel-->
        <Border
            Stroke="brown"
            StrokeShape="RoundRectangle 10, 10, 10, 10"
            StrokeThickness="5"
            Padding="5"
            >
            <VerticalStackLayout>
                <Label
                    Text="The Players"
                    FontSize="16" />
                <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>

                <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" />
                    <Label
                        Text="Player Settings"
                        FontSize="50"
                        FontAttributes="Italic"
                        HorizontalOptions="Center"
                        VerticalOptions="Center"/>
                </Grid>                
            </VerticalStackLayout>            
        </Border>
        
        <!--the Ending Conditions panel-->
        <Border
            Stroke="brown"
            StrokeShape="RoundRectangle 10, 10, 10, 10"
            StrokeThickness="5"
            Padding="5">
            <VerticalStackLayout>
                <Label
                    Text="Ending Conditions"
                    FontSize="16" />
                <Grid
                    RowDefinitions="*, *, *"
                    ColumnDefinitions="3*, 2*" >
                    <RadioButton
                        Content="The game is over when there is only one player with any money left."
                        GroupName="endingConditions" />
                    <RadioButton
                        Grid.Row="1"
                        Content="The game is over not later than after a given number of races."
                        GroupName="endingConditions" />
                    <Entry
                        Grid.Row="1"
                        Grid.Column="1" />
                    <Entry
                        Grid.Row="2"
                        Grid.Column="1" />
                    <RadioButton
                        Grid.Row="2"
                        Content="The game is over not later than after the racing time you set has elapsed."
                        GroupName="endingConditions" />
                </Grid>
            </VerticalStackLayout>
        </Border>
        
        <!--the Ready button-->
        <Button 
            Text="Ready"
            WidthRequest="200"
            VerticalOptions="Center" 
            FontSize="18" />
    </VerticalStackLayout>
</ContentPage>

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.

The PlayerSettings ContentView

We’ll put our content views in a Controls folder, so add this folder to the root of the app. But how do we create a content view? Well, right-click the Controls folder and select Add -> New Item… Then select the .NET MAUI ContentView (XAML) template, name it PlayerSettings.xaml and hit Add.

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:

<?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 (we’re going to talk about namespaces in XAML a bit later in the series) 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:

<?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:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Views.SettingsPage">
    <VerticalStackLayout
        ...
                <Grid
                    RowDefinitions="*"
                    ColumnDefinitions="100, 3*, 2*"
                    >
                    <Label
                        Grid.Column="1"
                        Text="Name" />
                    <Label
                        Grid.Column="2"
                        Text="Initial Money" />                    
                </Grid>                
                <VerticalStackLayout>
                    <controls:PlayerSettings />
                    <controls:PlayerSettings />
                    <controls:PlayerSettings />
                    <controls:PlayerSettings />
                </VerticalStackLayout>
            ...

Now the page looks like so:

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:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    ...
    <ShellContent
        Title=""
        ContentTemplate="{DataTemplate local:RacePage}"
        Route="RacePage" />
</Shell>

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.

So, before a race the page should look like this:

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 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, like in the image above. If you selected the second ending condition (the game is over not later than after a given number of race), you will see the following info displayed:

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 this:

We will implement the version for the second ending condition now, and later in the series we’ll implement a mechanism for displaying the correct data.

– Slugs’ Stats – here the statistics related to the slugs will be displayed. There are going to be four slugs in the game. Their names are Speedster, Trusty, Iffy and Slowpoke. In this panel you will see how many races each of them has won, expressed both as an actual number of wins and as a percentage.

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.

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.

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.

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:

<?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.RacePage">
    <Grid
        RowDefinitions="1.3*, 2*, 2*"
        ColumnDefinitions="3*, 3*, 3*, 2*">
        
        <!--Game Info-->
        <Border            
            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>
        
        <!--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>

Run the app and you will see the placeholders:

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

Add content view files using the .NET MAUI ContentView (XAML) template to the Controls folder. The folder should now look like so:

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.

The GameInfo ContentView

The GameInfo content view is the easiest one to implement. It’s not going to contain any nested content views. Here’s the code in the GameInfo.xaml file:

<?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.GameInfo">
    <Grid
        RowDefinitions="*, *, *, *, *"
        ColumnDefinitions="3*, *">
        <Label
            Text="Game Info" 
            FontAttributes="Bold" />
        <Label
            Grid.Row="1"
            Text="Race No:" />
        <Label
            Grid.Row="1"
            Grid.Column="1"
            Text="3" />
        <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:

<?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:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Views.RacePage">
    <Grid
        ...
        <!--Game Info-->
        <Border            
            Stroke="brown"
            StrokeShape="RoundRectangle 10, 10, 10, 10"
            StrokeThickness="5"
            Padding="5">
            <controls:GameInfo/>
        </Border>
        
        <!--Slugs' Stats-->
        ...

That’s it. Run the app and watch the Game Info panel:

Let’s take care of the Slugs‘ Stats panel next.

The SlugsStats and SlugStats ContentViews

The SlugsStats content view is a bit more complicated than GameInfo in that it will contain nested content views. I named the view SlugsStats (with the plural form Slugs at the beginning) because this view will display data for all the slugs. The nested view will display data for just one slug, so let’s name it SlugStats (with the singular form Slug at the beginning). For easy alignment, we’ll implement the SlugStats view as a one-row grid. Actually, let’s start with the nested view. So, add a new content view file to the Controls folder and name it SlugStats.xaml. Then implement the code in this file like this:

<?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.SlugStats">
    <Grid
        RowDefinitions="*"
        ColumnDefinitions="2*, *, *">
        <Label
            Text="Slug Name" />
        <Label
            Grid.Column="1"
            Text="4 wins" />
        <Label
            Grid.Column="2"
            Text="46%" />
    </Grid>
</ContentView>

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:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Controls.SlugsStats">
    <Grid
        RowDefinitions="*, *, *, *, *"
        ColumnDefinitions="*">
        <Label
            Text="Slugs' Stats"
            FontAttributes="Bold" />
        <controls:SlugStats 
            Grid.Row="1" />
        <controls:SlugStats 
            Grid.Row="2" />
        <controls:SlugStats 
            Grid.Row="3" />
        <controls:SlugStats 
            Grid.Row="4" />
    </Grid>
</ContentView>

Finally, we can add the SlugsStats view to the RacePage:

<?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:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Views.RacePage">
    <Grid
        ...
        <!--Slugs' Stats-->
        <Border 
            ...>
            <controls:SlugsStats />
        </Border>
        
        <!--Players' Stats-->
        ...

Run the app and watch the control (with dummy data for now) in action:

Next, let’s implement the PlayersStats and PlayerStats content views.

The PlayersStats and PlayerStats ContentViews

We’ll implement the PlayersStats view in a very similar way as the SlugsStats view. It will also contain nested views. The nested view will be named PlayerStats. Add a content view file named PlayerStats.xaml to the Controls folder and implement the code like this:

<?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.PlayerStats">
    <Grid
        RowDefinitions="*"
        ColumnDefinitions="2.5*, 1.5*">
        <Label
            Text="Player Name" />
        <Label
            Grid.Column="1"
            Text="has $1000" />
    </Grid>
</ContentView>

Next, let’s implement the PlayersStats like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Controls.PlayersStats">
    <Grid
        RowDefinitions="*, *, *, *, *"
        ColumnDefinitions="*">
        <Label
            Text="Players' Stats"
            FontAttributes="Bold" />
        <controls:PlayerStats 
            Grid.Row="1" />
        <controls:PlayerStats 
            Grid.Row="2" />
        <controls:PlayerStats 
            Grid.Row="3" />
        <controls:PlayerStats 
            Grid.Row="4" />
    </Grid>
</ContentView>

And finally, let’s add the PlayersStats control to the RacePage:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <Grid
        ...
        <!--Players' Stats-->
        <Border    
            ...>
            <controls:PlayersStats />
        </Border>
        
        <!--the buttons-->
        ...

Run the app and watch the Players’ Stats panel.

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.

The Graphical Assets

We’ll need a couple of images. First, let’s add the image of the slimy track. It’s saved as racetrack.png. The dimensions of the image are 1000 x 200 px. It looks like this:

Next, we’ll need the top-view images of the slugs, separately their bodies and tentacles. These images look like so:

speedster_body.png

speedster_eye.png

trusty_body.png

trusty_eye.png

iffy_body.png

iffy_eye.png

slowpoke_body.png

slowpoke_eye.png

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:

speedster.png

trusty.png

iffy.png

slowpoke.png

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:

Now we can use the images in our content views.

The Racetrack, SlugImage, TrackImage, SlugInfo and WinnerInfo ContentViews

The Racetrack will be implemented as a content view that consists of several other content views. Let’s start by adding the following content views to the Controls 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:

<?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.SlugImage">
    <AbsoluteLayout>
        <Image 
            Source="speedster_eye.png" 
            AbsoluteLayout.LayoutBounds="1.2, .35, .25, .23"
            AbsoluteLayout.LayoutFlags="All"
            Rotation="-30"
            AnchorX="0"/>
        <Image 
            Source="speedster_eye.png" 
            AbsoluteLayout.LayoutBounds="1.2, .65, .25, .23"
            AbsoluteLayout.LayoutFlags="All"
            Rotation="30"
            AnchorX="0"/>
        <Image 
            Source="speedster_body.png" 
            AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
            AbsoluteLayout.LayoutFlags="All" 
            Aspect="Fill"/>
    </AbsoluteLayout>
</ContentView>

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:

So, if we didn’t set the AnchorX property on the eye images, the slug would end up looking like this:

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:

This is why we set AnchorX to 0 and we left AnchorY out, allowing for its default value of 0.5. Now the slug looks good:

Another new property that we set on the image of the slug’s body is Aspect. There are a couple options you can set this property to. The property is used to indicate how the image will fit into the display area. If you set it to Fill, the image will be stretched to completely fill the display area. This way the eyes won’t get detached from the body no matter how much you stretch or squeeze the image:

Stretching wouldn’t look so good if you set Aspect to a different value. For example, if you set it to AspectFit, everything looks fine until the image is stretched. Then it turns into something like this:

Poor kid.

This is why this property is set to Fill.

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:

<?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.SlugInfo">
    <Grid 
        Padding="5, 2, 0, 2"
        RowDefinitions="*, *"
        ColumnDefinitions="6.8*, 2.2*">
        <Label 
            Text="Speedster"
            FontSize="18"
            FontAttributes="Bold"
            TextColor="White"/>
        <Label 
            Grid.Row="1"
            Text="0 wins"
            FontSize="14"
            TextColor="White"/>
        <Label 
            Grid.Column="1"
            Grid.RowSpan="2"
            VerticalOptions="Center"
            Text="1.40"
            FontSize="40"
            FontAttributes="Bold"
            TextColor="White"/>
    </Grid>    
</ContentView>

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:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Controls.TrackImage">
    <AbsoluteLayout>
        <!--Racetrack-->
        <Image
            Source="racetrack.png"
            AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
            AbsoluteLayout.LayoutFlags="All"
            Aspect="Fill"/>
        
        <!--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:

<?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.WinnerInfo">    
    <Grid
        RowDefinitions=".6*, *, 3*">
        <Label 
            Text="The winner is"
            FontSize="30"
            HorizontalOptions="Center"
            FontAttributes="Bold" />
        <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:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Controls.Racetrack">
    <Grid
        ColumnDefinitions="9*, 2*">
        <controls:TrackImage
            Margin="20, 0, 0, 0" />
        <controls:WinnerInfo
            Grid.Column="1" />
    </Grid>
</ContentView>

Finally, we can place the whole big and complex Racetrack content view in the RacePage:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <Grid
        ...
        <!--Racetrack-->
        <Border    
            ...>
            <controls:Racetrack />
        </Border>
        
        <!--Bets/Results panel-->
        ...

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.

The Bets and PlayerBet ContentViews

We already created the Bets.xaml file. Inside the Bets view we’ll embed controls that will enable the players to place their bets. There will be one such control for each player. We haven’t created it yet, so go ahead and add a new ContentView to the Controls folder. Name it PlayerBet. It will contain the player’s name, an entry and a slider for setting the amount of money the player wants to bet and four radio buttons, one for each slug, so that the player can check the slug they want to bet on.

Here’s the code in the PlayerBet.xaml file:

<?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.PlayerBet">
    <Grid
        ColumnDefinitions="*, .7*, .1*, .7*, 1.5*, .3*, *, *, *, *">
        <Label 
            Text="Player 1" />        
        <Label
            Grid.Column="1"
            Text="bets" />
        <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:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Controls.Bets">
    <Grid
        RowDefinitions="*, 4*, *">
        <Label 
            Text="Bets"
            FontAttributes="Bold"/>
        <VerticalStackLayout
            Grid.Row="1">
            <controls:PlayerBet />
            <controls:PlayerBet />
            <controls:PlayerBet />
            <controls:PlayerBet />
        </VerticalStackLayout>
        
        <Button
            Grid.Row="2"
            Text="Go"
            HorizontalOptions="Center" />
    </Grid>
</ContentView>

Finally, let’s add the Bets view to the RacePage:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             ...>
    <Grid
        ...
        <!--Bets/Results panel-->
        <Border    
            ...>
            <controls:Bets />
        </Border>
        
    </Grid>
</ContentPage>

Run the app and watch the Bets panel:

After each race, the Bets view will be replaced by the Results view. So, let’s create it next.

The Results and PlayerResult ContentViews

The Results view will look very much like the Bets view. We’ll also embed in it other views that will present the results of the particular players. To this end, let’s create a new ContentView and name it PlayerResult. The PlayerResult.xaml file should contain the following code:

<?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.PlayerResult">
    <Grid
        ColumnDefinitions="*, *, *, *, *, *, *">
        <Label 
            Text="Player 1" />
        <Label
            Grid.Column="1"
            Text="had $1000," />
        <Label
            Grid.Column="2"
            Text="bet $250" />
        <Label 
            Grid.Column="3"
            Text="on Speedster,"/>
        <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:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Slugrace.Controls"
             x:Class="Slugrace.Controls.Results">
    <Grid
        RowDefinitions="*, 4*, *">
        <Label 
            Text="Results"
            FontAttributes="Bold"/>
        <VerticalStackLayout
            Grid.Row="1">
            <controls:PlayerResult />
            <controls:PlayerResult />
            <controls:PlayerResult />
            <controls:PlayerResult />
        </VerticalStackLayout>

        <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:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             ...>
    <Grid
        ...
        <!--Bets/Results panel-->
        <Border    
            ...>
            <controls:Results />
        </Border>
        
    </Grid>
</ContentPage>

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:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell...>

    <ShellContent
        Title=""
        ContentTemplate="{DataTemplate local:GameOverPage}"
        Route="GameOverPage" />
</Shell>

This page is only going to display some labels and buttons. The GameOverPage.xaml file should look like this:

<?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.GameOverPage">
    <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
            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 will be fixed soon. They don’t look pretty, either. In the next part of the series we’ll be styling them so that they really shine.


Spread the love
Tags:

Leave a Reply