Skip to content
Home » Basics of .NET MAUI – Part 19 – Navigation

Basics of .NET MAUI – Part 19 – Navigation

Spread the love

In the previous part of the series 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’ll navigate to the RacePage. Well, this is what we want the button to do. For now it does nothing.

So, in this part 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 SettingsPage to RacePage when the Ready button is clicked,

– 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 RacePage to GameOverPage when the game is over,

– from GameOverPage to SettingsPage when the Play Again button in the former is pressed.

Besides, only one of the Bets and Results content views can be visible at a time.

We’ll implement all this functionality in this part. We’ll also create the missing view models and a basic InstructionsPage as we proceed.

To implement navigation to the GameOverPage, we’ll create a basic game simulation.

To practice navigation, we’ll start by implementing a navigation system between the SettingsPage and TestPage. For now, when the Ready button is clicked, we’ll navigate to the TestPage and we’ll be able to navigate back to the SettingsPage from there. So, let’s begin.

Navigation to Another Page

Let’s simplify the TestPage.xaml file like so:

Also, we don’t need the properties we defined before in the TestViewModel. Make sure this class looks for now like so:

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:

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 in the former is pressed. Let’s create a method in the SettingsViewModel and add a RelayCommand attribute to it:

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:

Now go to the SettingsPage and bind the button’s Command property to the method:

Now run the app, fill in some valid data and hit the Ready button. This will take you to the TestPage. Here’s what it looks like on Windows:

You can see a little arrow in the upper left corner. If you click it, you will navigate back to the SettingsPage. Here’s what it looks like on Android:

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.

Passing Data Between Pages

We can pass information from page to page. To do that, we can define query parameters or a dictionary. Let’s have a look at the first approach. It’s used for simple data types, like strings, ints, etc.

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:

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:

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:

We can use nameof for the first argument as well:

Let’s now bind to this property. We’ll create a new label in the TestPage view and bind its Text property to MaxLimit:

Now you can see the value displayed correctly:

You can also pass multiple query parameters. Let’s modify the StartGame method:

Here we added a second parameter. Now, let’s receive both parameters in the TestViewModel:

Let’s bind to this property, too:

Run the app and you’ll see this:

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:

How do we receive navigation data in the TestViewModel now? Exactly the same as before:

Let’s bind to it:

If we set the first player’s initial money to 2000 and hit the Ready button, we’ll see the following:

Fine. But what if we want to navigate back?

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 TextPage.xaml file:

If you now run the app, you won’t see the back button anymore.

Let’s 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:

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.

And here’s the button in the TestPage view:

If you run the app, you’ll see the button:

If you hit it, you’ll go back to where you came from.

It’s time to implement navigation in our app. But first let’s create the view model for the RacePage.

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:

And now add a GameViewModel class to the ViewModels folder and implement it like so:

Let’s register the page and the view model with the dependency service in MauiProgram.cs:

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:

We want to be able to navigate to RacePage, so let’s register the route in AppShell.xaml.cs:

We removed the route to the TestPage because we don’t need it anymore.

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:

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.

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:

And here’s the Game class:

We’ll discuss all the new properties when we need them. With that in place, let’s create the StartGame method in the SettingsViewModel:

Here we’re populating the Game object with the data collected from the SettingsPage. This object is then passed to the RacePage.

We can now bind the method to the Command property of the Ready button in the SettingsPage:

Next, in the GameViewModel, we have to add the QueryProperty attribute and create a property to store the data passed from the SettingsPage:

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:

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:

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:

We’re done. If you now run the app, select the Money ending condition and press the Ready button, you will only see the race number in the Game Info panel:

If you select the Races ending condition and set the number of races to 20, you will see only races-related data:

Finally, if you select the Time ending condition and set the time to 45 minutes, you will see only time-related data:

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:

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:

This message will be sent each time the race status changes to Finished. Let’s modify the RaceStatus property in the GameViewModel:

The message is registered in the SlugViewModel class’s constructor. In the OnRaceFinishedMessageReceived method WinPercentage is calculated after each race. Also the Odds and PreviousOdds are recalculated depending on whether the slug won the race or not. The odds decrease if the slug wins and increase otherwise.

We also have the RecalculateStats method that we will call later from inside the GameViewModel for each slug.

This is all about slugs. What about the players?

PlayerViewModel

Add a PlayerViewModel class. It will contain all player-related data:

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:

The latter should be implemented like this:

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:

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.

Slugs’ Stats and Slug Stats

In the Slugs’ Stats panel we can view the achievements of all the slugs. The binding context for this content view is GameViewModel. There we use the SlugStats content views for each slug. Each SlugStats control binds to one slug:

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:

The stats will be recalculated after each race. We’ll implement this in a moment when we create a game simulation. For now, though, let’s move on to the Players’ Stats panel.

Players’ Stats and Player Stats

The binding context for the PlayersStats content view is GameViewModel. For the slugs we added four SlugStats controls to the SlugsStats view because there are always four slugs. But the number of players may vary between one and four, so we’ll add a CollectionView and set the Players as its source. We’ll use the PlayerStats control as the item template:

And now let’s have a look at a single player. The binding context of the PlayerStats control is PlayerViewModel:

Here we use the InvertedBoolConverter from the Toolkit.

Let’s move on to the racetrack now. There we have the slug images and some slug info.

Slug Image and Slug Info

The binding context for the SlugImage and SlugInfo content views is SlugViewModel. In the SlugImage control we bind to the top-view image properties:

The SlugInfo content view is used to display the slugs’ names, wins and odds:

Next, let’s move on to the Bets and Results panels.

Bets and Results

We’ll use GameViewModel as the binding context for the Bets and Results views. For the PlayerBet and PlayerResult controls we’ll use PlayerViewModel.

In the Bets content view we’ll use a CollectionView for the individual players:

You can see the Command property on the button. We’re going to implement the StartRace method in the view model in a moment.

The PlayerBet control looks a bit complex:

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:

Now, back to the XAML file. We bind the radio buttons to the SelectedSlug property of PlayerViewModel using a converter we created. Add the SelectedSlugToBoolConverter to the Converters folder and implement it like so:

Now we can pass the names of the slugs as converter parameters.

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.

Next, let’s have a look at the Results content view. It’s implemented in the same way as the Bets view:

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. We’ll implement the IsShowingFinalResults property in the GameViewModel when we simulate a game.

A single PlayerResult control is implemented like this:

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, the Go button in the Bets content view binds to the StartRace command in the GameViewModel.

Let’s add this method to GameViewModel:

This method just sets the RaceStatus to Started and calls the RunRace method. 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 the type IDispatcherTimer like so:

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 have a look at this method in a moment. For now, let’s see what happens next.

Next, the RunRace method is called. Here it is:

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:

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:

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:

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:

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

For the GameOverPage we’ll create a GameOverViewModel. Here’s the code:

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.

We set the binding context in the code-behind file of the GameOverPage:

The GameOverPage looks like so:

Let’s register the the GameOverPage and the GameOverViewModel with the dependency service in the MauiProgram.cs file:

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 wound be navigated to immediately.

Fine, let’s implement the timer then:

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 the NextRace method in GameViewModel. Here it is:

Here the race number is increased, the race status is reset to NotYetStarted and each player is reset.

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:

If you now hit the Ready button, you’ll navigate to the RacePage:

The End Game and Instructions buttons are disabled because they’re only enabled when the RaceStatus is Finished. The Go button is disabled because we have to set the bet amount for each player and select the slugs, for example like this:

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.

Try it out with other numbers of players and ending conditions. It should work fine.

And now let’s run the app on Android. Here’s the SettingsPage with the same settings as before:

Let’s hit the Ready button:

And now let’s place some bets:

Let’s press the Go button. Now we have one minute to play. Here’s what we can expect to see during the game:

The GameOverPage looks like this:

Our game simulation works fine. Before we implement the actual game flow, though, let’s take care of the End Game and Instructions buttons in RacePage.

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.

Let’s create an EndGameManually method in the GameViewModel:

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:

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.

Finally, let’s handle the Instructions button.

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 class to the Views folder, name it InstructionsPage, and implement it like so:

In the code-behind let’s set the binding context to the GameViewModel:

Let’s also register the page with the dependency service in MauiProgram.cs:

In the GameViewModel, let’s define two methods, one for navigating to the InstructionsPage and one for navigating back:

We mustn’t forget to add the route in the AppShell.xaml.cs file:

The last piece of the puzzle is to bind the Instructions button in RacePage to the SeeInstructions method:

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:

If we click the Back button, we’ll go back to RacePage.

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 part of the series.


Spread the love
Tags:

Leave a Reply