Skip to content
Home » Basics of .NET MAUI – Part 22 – Popups

Basics of .NET MAUI – Part 22 – Popups

Spread the love

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.

So, let’s start with the first popup.

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:

So, first of all, we changed the type from ContentView to toolkit:Popup.

We also set the CanBeDismissedByTappingOutsideOfPopup property to false. 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:

As you can see, 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:

And here’s the code-behind:

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:

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:

We’re done with this one. Now let’s move on to implementing accidents.

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 a slug and even help him win.

Anyway, whatever happens, it will only have consequences in the race in which it happens. In the next race the affected slug will be up and running again.

Each accident will have a graphical representation, which you can see below. Later, will 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

Overheating means stopping and not being able to continue the race either.

Heart Attack

If a slug suffers from a heart attack, it’s definitely not convenient either. The race is over for that slug. He needs a rest. His heart is beating like crazy, which you can see and hear. You will see a small heart image on top of the affected slug.

Power Grass

This is the first accident that may be very convenient actually. First, some magic grass appears on the racetrack, just in front of the slug, and he stops to eat it, which takes a while. But the grass powers him up and then he starts racing much faster than before.

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

Drowning sucks. When this accident happens, a puddle of water appears on the racetrack and when the slug enters it, he drown. The slug becomes less and less visible under the water until he disappears completely.

Electroshock

An electroshock is good, at least for a slug. Not only doesn’t it kill him, but it even speeds him up considerably, which often results in a vistory. You’ll see a pulsating bolt on the slug’s back.

Turning Back

Sometimes a slug forgets something and turns back. By doing so he looses his chances of winning.

Slug Monster

Finally, a slug may be eaten by the horrifying  slug monster. Being devoured is never good.

So, these are all the accidents that may happen in the game. Now we’ll implement some classes that we will need for 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.

And now let’s start coding the accidents.

Accident Model Class

Let’s start by adding a new class to the Models folder and naming it Accident. Here’s the code:

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 information about 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 file in the ViewModels folder and name it AccidentViewModel. Here’s the code:

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.

An interesting property is Expected. It returns a boolean value 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.

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:

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. I doubled the slugs’ running times to make them run slower. Now it’s time to implement the accident logic:

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 Expected property is set to true. As you remember, this property is set randomly.

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.

Next, a random slug is picked to be affected by the accident.

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.

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:

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

Handling the Accidents

The accidents will be implemented as animations, so we’ll write the code in the code-behind. Let’s start by opening the TrackImage.xaml file and making sure the layout and racetrack elements are given names so that we can reference them in code:

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:

In the constructor we assign images to the particular variables.

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 make some changes in the HandleRunning method to reset before each race the properties modified by an accident, like ZIndex, Opacity or ScaleX and also remove the accident image:

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 that will show up whenever an accident happens.

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:

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:

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.

Here’s the code-behind:

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:

We need a way to display the popup. To this end, we’ll add a DisplayAccidentPopup method to the GaveViewModel:

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.

If you click anywhere outside the popup, it will be dismissed.

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:

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.

Next, we modify the PlaySound method. It now takes an additional loopingAccidentSound parameter of type bool. If it’s set to false, the IAudioPlayer is added to the effectPlayers list. Otherwise, it’s added to the loopingAccidentPlayers list.

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:

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

And now let’s implement the accidents one by one.

Accidents Implementation

Each accident will be implemented in a separate asynchronous method in the TrackImage class. Let’s start with the Broken Leg accident.

Broken Leg Accident

This accident will be implemented inside the HandleBrokenLeg method. When the race begins, nothing happens until the accident’s TimePosition is reached, which is a randomized value and may differ from race to race within a certain range.

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.

Here’s the code:

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:

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.

So, let’s take care of the healing process.

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. Here’s the Slug class:

We also need to implement the default image properties in the SlugViewModel:

Next, let’s go to the SettingsViewModel and set the properties for each slug there:

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:

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. Then, in the TrackImage class add the HandleOverheat method:

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:

Then the body and eye images are replaced so that the slug really looks as if he was burned. Naturally, 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:

Naturally, the images will be reset in the next race. And now let’s move on to a more complicated accident.

Heart Attack Accident

In this accident we’ll use an accident image. It will be set differently for the Windows and Android platforms so that it looks similar on both platforms. Here’s the code:

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.

If the accident happens, we should see something like this:

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 then before. Here we have the grass image, which will be scaled and positioned differently for each platform. Anyway, here’s the code:

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:

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 (Trusty) is slightly bigger than the other slugs, but it’s hard to notice in a static image like below:

In the next accident, the slug will go blind.

Blind Accident

From time to time, a slug may lose his tentacles and go blind. Here’s how this accident is implemented:

The slug doesn’t stop running when the accident happens. He just loses his eyes (the eye image is set to null) and starts moving in a seemingly chaotic way, just like Slowpoke in the image below:

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:

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. Here’s what it looks like:

Let’s move on to the next accident.

Electroshock Accident

The next accident, Electroshock, is implemented like so:

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.

Here’s a slug just being struck by lightning:

Sometimes nothing special happens to a slug, but they just turn back. Let’s implement this.

Turning Back Accident

Here’s the Turning Back accident:

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.

Here’s the effect:

And there is one more accident, the most terrible one…

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 racetrack is. The slug catches the slug and eats it. Here’s how this is implemented in our app:

The slug monster comes from the left-hand side of the window. Here’s what it looks like:

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


Spread the love

Leave a Reply