© Vincent Maverick S. Durano 2019
Vincent Maverick S. DuranoUnderstanding Game Application Development https://doi.org/10.1007/978-1-4842-4264-3_4

4. Building Mobile Application with Xamarin.Forms

Vincent Maverick S. Durano1 
(1)
Minnetonka, MN, USA
 
Now that we have the API ready, we can start implementing the Memory Game mobile application and start consuming the Web API that we’ve just created in the Chapter 3. Go head and navigate to MemoryGame.App project and then create the following folders:
  • REST – this folder is where we put the class for managing REST API calls.

  • Services – this folder is where we put the interfaces that the application is going to need.

  • Classes – this folder is where application-specific classes such as Helpers, Settings, and Data Manager are stored.

  • Pages – this folder is where the XAML files needed for the application are stored.

We are doing it this way in order for us to easily manage the files by just looking at the folder for ease of navigation and maintainability.

Implementing the Service Interfaces

We are going to use an interface to define a common method in which other applications can implement it. This is because Android and iOS platforms have different code implementations to deal with device vibration, playing a sound, and storage.

An interface is just a skeleton of a method without the actual implementation. This means that the application which implements the interface will create a class to perform a concrete platform-specific implementation.

The IHaptic Interface

Let’s create a few services that our app will need. Let’s start by adding the IHaptic interface. To do this, right-click the Services folder and then select AddNew Item. On the left pane under Visual C# ItemsCode, select Interface and name it “IHaptic.cs” just like in the following figure:
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig1_HTML.jpg
Figure 4-1

Adding a new interface file

Click Add and then replace the default generated code with the following code:
namespace MemoryGame.App.Services
{
    public interface IHaptic
    {
        void ActivateHaptic();
    }
}

The preceding code is nothing but a simple interface definition that contains a void method called ActivateHaptic() .

The ILocalDataStore Interface

Create another interface file under the Services folder. Name the file as “ILocalDataSource.cs” and replace the default generated code with the following:
namespace MemoryGame.App.Services
{
    public interface ILocalDataStore
    {
        void SaveSettings(string fileName, string text);
        string LoadSettings(string fileName);
    }
}

The ILocalDataStore interface contains two main methods: SaveSettings() takes a file name and a text as the parameter. The LoadSettings() method takes a file name as a parameter and returns a string type.

The ISound Interface

Lastly, create another interface and name it “ISound.cs”. Replace the default generated code with the following:
namespace MemoryGame.App.Services
{
    public interface ISound
    {
        bool PlayMp3File(string fileName);
        bool PlayWavFile(string fileName);
    }
}

The ISound interface contains two main boolean methods: PlayMp3File() and PlayWavFile() , which takes a file name as the parameter.

The reason we are creating the preceding services/interfaces is because iOS and Android have different code implementations for setting device vibration and sound. That’s why we are defining interfaces so both platforms can just inherit from it and implement code-specific logic.

Let’s move on by creating the following files within the Classes folder:
  • Helper.cs

  • Settings.cs

  • PlayerManager.cs

  • MemoryGame.cs

The Helper Class

Create a new class called “Helper.cs” under the Classes folder and then replace the default generated code with the following code:
using Plugin.Connectivity;
namespace MemoryGame.App.Helper
{
    public static class StringExtensions
    {
        public static int ToInteger(this string numberString)
        {
            int result = 0;
            if (int.TryParse(numberString, out result))
                return result;
            return 0;
        }
    }
    public static class Utils
    {
        public static bool IsConnectedToInternet()
        {
            return CrossConnectivity.Current.IsConnected;
        }
    }
}

The Helper.cs file is composed of two classes: StringExtension and Utils . The StringExtension class contains a ToIntenger() extension method that accepts a string containing a valid numerical value and converts it into an integer type. The Utils class, on the other hand, contains an IsConnectedToInternet() method to verify Internet connectivity. We will be using these methods later in our application.

The Settings Class

Create another class within the Classes folder and name it “Settings.cs”. Replace the default generated code with the following:
using Plugin.Settings;
using Plugin.Settings.Abstractions;
using System;
namespace MemoryGame.App.Classes
{
    public static class Settings
    {
        private static ISettings AppSettings => CrossSettings.Current;
        public static string PlayerFirstName
        {
            get => AppSettings.GetValueOrDefault(nameof(PlayerFirstName), string.Empty);
            set => AppSettings.AddOrUpdateValue(nameof(PlayerFirstName), value);
        }
        public static string PlayerLastName
        {
            get => AppSettings.GetValueOrDefault(nameof(PlayerLastName), string.Empty);
            set => AppSettings.AddOrUpdateValue(nameof(PlayerLastName), value);
        }
        public static string PlayerEmail
        {
            get => AppSettings.GetValueOrDefault(nameof(PlayerEmail), string.Empty);
            set => AppSettings.AddOrUpdateValue(nameof(PlayerEmail), value);
        }
        public static int TopScore
        {
            get => AppSettings.GetValueOrDefault(nameof(TopScore), 1);
            set => AppSettings.AddOrUpdateValue(nameof(TopScore), value);
        }
        public static DateTime DateAchieved
        {
            get => AppSettings.GetValueOrDefault(nameof(DateAchieved),
DateTime.UtcNow);
            set => AppSettings.AddOrUpdateValue(nameof(DateAchieved),
value);
        }
        public static bool IsProfileSync
        {
            get => AppSettings.GetValueOrDefault(nameof(IsProfileSync),
false);
            set => AppSettings.AddOrUpdateValue(nameof(IsProfileSync),
value);
        }
        public static int PlayerID
        {
            get => AppSettings.GetValueOrDefault(nameof(PlayerID), 0);
            set => AppSettings.AddOrUpdateValue(nameof(PlayerID), value);
        }
    }
}

The Settings.cs file contains a few static properties that we are going to use in the application. They are defined static so that we don’t need to create an instance of the class when calling them; that’s what the Helper or Utility class is meant for. We are defining them in the Settings.cs file to separate them from the Helper class for one sole purpose: having a central location for shared properties that can be accessed across all our applications. You can think of it as a local data store for the user’s data and application configuration.

Let’s look at a quick example:
public static string PlayerFirstName
{
    get => AppSettings.GetValueOrDefault(nameof(PlayerFirstName),
string.Empty);
    set => AppSettings.AddOrUpdateValue(nameof(PlayerFirstName),
value);
}

The PlayerFirstName is a static property that contains Expression-Bodied Members for getting and setting values. Expression-bodied functions are another syntax simplification in C# 6.0. These are functions with no statement body. Instead, you implement them with an expression following the function declaration.

This code is an example of expression body definition:
get => AppSettings.GetValueOrDefault(nameof(PlayerFirstName), string.Empty);

The preceding code gets the value of PlayerFirstName and sets a default value to string.Empty.

C# 7.0 introduces this syntax for setters:
set => AppSettings.AddOrUpdateValue(nameof(PlayerFirstName), value);

The preceding code sets the PlayerFirstName with the new value assigned and stores it locally in the device.

For more information on Expression-Bodied Members, read the following: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members

The Settings plug-in saves specific properties directly to each platform’s native setting APIs (NSUserDefaults in iOS, SharedPreferences in Android, etc.). This ensures the fastest, securest, and most reliable creation and editing settings per application.

For more information about the Settings plug-in, see the following: https://jamesmontemagno.github.io/SettingsPlugin/

The DTO Class

Create another class within the Classes folder and name it “DTO.cs”. Replace the default generated code with the following:
using System;
namespace MemoryGame.App.Classes
{
    public class PlayerProfile
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }
    public class PlayerScore
    {
        public int ChallengerID { get; set; }
        public byte Best { get; set; }
        public DateTime DateAchieved { get; set; }
    }
    public class PlayerData
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public byte Best { get; set; }
        public DateTime DateAchieved { get; set; }
    }
}

The DTO file contains three main classes: The PlayerProfile, PlayerScore, and PlayerData. We will use these classes as DTOs for passing information from one place to another.

The GameAPI Class

Since we finished creating the Web API earlier, it’s time for us to create a class that consumes the API endpoints. Create a new class called “GameAPI.cs” under the REST folder and then replace the default generated code with the following code:
using System;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using MemoryGame.App.Classes;
using System.Net.Http;
using System.Net.Http.Headers;
namespace MemoryGame.App.REST
{
    public class GameAPI
    {
        //replace the value of APIUri with the published URI to where your API is hosted.
        //E.g http://yourdomain.com/yourappname/api/game
        private const string APIUri = "http://localhost:56393/api/game/players";
        HttpClient client;
        public GameAPI()
        {
            client = new HttpClient();
            client.MaxResponseContentBufferSize = 256000;
            client.DefaultRequestHeaders.Clear();
            //Define request data format
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }
        public async Task<bool> SavePlayerProfile(PlayerProfile data, bool isNew = false)
        {
            var uri = new Uri(APIUri);
            var json = JsonConvert.SerializeObject(data);
            var content = new StringContent(json, Encoding.UTF8,"application/json");
            HttpResponseMessage response = null;
            if (isNew)
                response = await ProcessPostAsync(uri, content);
            if (response.IsSuccessStatusCode)
            {
                Settings.IsProfileSync = true;
                return true;
            }
            return false;
        }
        public async Task<bool> SavePlayerScore(PlayerScore data)
        {
            var uri = new Uri($"{APIUri}/score");
            var json = JsonConvert.SerializeObject(data);
            var content = new StringContent(json, Encoding.UTF8,"application/json");
            var response = await ProcessPostAsync(uri, content);
            if (response.IsSuccessStatusCode)
                return true;
            return false;
        }
        public async Task<int> GetPlayerID(string email)
        {
            var uri = new Uri($"{APIUri}/{email}/");
            int id = 0;
            var response = await ProcessGetAsync(uri);
            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                id = JsonConvert.DeserializeObject<int>(content);
            }
            return id;
        }
        public async Task<PlayerData> GetPlayerData(string email)
        {
            var uri = new Uri($"{APIUri}/profile/{email}/");
            PlayerData player = null;
            var response = await ProcessGetAsync(uri);
            if (response.IsSuccessStatusCode)
            {
                player = new PlayerData();
                var content = await response.Content.ReadAsStringAsync();
                player = JsonConvert.DeserializeObject<PlayerData>(content);
            }
            return player;
        }
        private async Task<HttpResponseMessage> ProcessPostAsync(Uri uri, StringContent content)
        {
            return await client.PostAsync(uri, content);
        }
        private async Task<HttpResponseMessage> ProcessGetAsync(Uri uri)
        {
            return await client.GetAsync(uri);
        }
    }
}

The preceding code is pretty much self-explanatory, as you could probably guess by its method name. The class just contains a method that calls the API endpoints that we created in the previous section. If the code does not make sense to you now, don’t worry as we will talk about it later in this section.

Async and Await Overview

Before we dig into the code implementation details of the GameAPI class, let’s have a quick overview of the Async and Await concepts in C#.

Asynchronous programming is all about writing code that allows several things to happen at the same time without “blocking,” or waiting for other things to complete. This is different from synchronous programming, in which everything happens in the order it is written. In order to perform an asynchronous operation, a method should be marked as async: this tells the compiler that the method can run asynchronously. The await keyword is used in conjunction with the async keyword to tell the compiler to wait for the Task without blocking code execution.

The async keyword only enables the await keyword. The await keyword is where things can get asynchronous. Await is like a unary operator: it takes a single argument, an awaitable Task or Task<T> (an “awaitable” is an asynchronous operation). Await examines that awaitable to see if it has already completed; if the awaitable has already completed, then the method just continues running (synchronously, just like a regular method).

The Task and Task<T> represent an asynchronous operation that can be waited. A Task does not return a value, while Task<T> does.

Note

If an async method doesn’t use an await operator to mark a suspension point, the method executes as a synchronous method does, despite the async modifier. The compiler issues a warning for such methods.

Here’s a brief definition of the async and await keywords taken from the official documentation here: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

The async and await keywords in C# are the heart of async programming . By using those two keywords, you can use resources in the .NET Framework, .NET Core, or the Windows Runtime to create an asynchronous method almost as easily as you create a synchronous method . Asynchronous methods that you define by using the async keyword are referred to as async methods.

Method Definitions

Now that you have a basic idea regarding how asynchronous programming works, let’s see what the code does by breaking them. Let’s start with the GameAPI constructor code block:
private const string APIUri = "http://localhost:56393/api/game/players";
HttpClient client;
public GameAPI()
{
    client = new HttpClient();
    client.MaxResponseContentBufferSize = 256000;
    client.DefaultRequestHeaders.Clear();
    //Define request data format
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

The APIUri is a private variable that holds the base API endpoint value. In this example, it uses the value http://localhost:56393/api/game/players, which points to my local development IIS Express host. The value “http://localhost:56393” is automatically created for you once you run the application in Visual Studio. You need to change this value with the published URI to where your API is hosted. We’ll talk more about that later in the Chapter 6.

It was my intent to put the value of APIUri within the GameAPI class for the sake of simplicity. In real-world applications, it is recommended to store the value of APIUri in a configuration file, where you can easily modify the value.

The HttpClient object is declared on the second line. HttpClient is a modern HTTP client for .NET. It provides a flexible and extensible API for accessing all things exposed through HTTP.

On the next line is the GameAPI class constructor. This is where the HttpClient is initialized and configured with a few properties for setting the MaxResponseContentBufferSize and DefaultRequestHeader media type.

Next code block:
public async Task<bool> SavePlayerProfile(PlayerProfile data, bool isNew = false)
{
    var uri = new Uri(APIUri);
    var json = JsonConvert.SerializeObject(data);
    var content = new StringContent(json, Encoding.UTF8,"application/json");
    HttpResponseMessage response = null;
    if (isNew)
        response = await ProcessPostAsync(uri, content);
    if (response.IsSuccessStatusCode)
    {
        Settings.IsProfileSync = true;
        return true;
    }
    return false;
}

The SavePlayerProfile() takes a PlayerProfile object and an optional bool isNew flag parameter. This method is defined as asynchronous by marking it as async and returns a Task of type bool.

Inside the method, we define a new Uri object that takes the APIUri as the parameter. We then serialize the data using Newtonsoft’s JsonCovert.SerializeObject() method and pass the result into a json variable. After the data has been serialized, we create a StringContent object with the format “application/json” and the encoding set to UTF8. The StringContent class creates a formatted text appropriate for the http server/client communication. After a client request, a server will respond with an HttpResponseMessage and that response will need a content; that can be created with the StringContent class.

In the next line, we create an instance of the HttpResponseMessage object and we check for the isNew flag to do some validation. If the flag value is true, we call an awaitable Task called ProcessPostAsync() and pass along the uri and content values. The awaitable Task return is then assigned to an HttpResponseMessage object called response. If the response is successful, then we set the value of Settings.IsProfileSync to true and return true to the method. Otherwise, if the flag value is false or the response isn’t successful, we simply return false to the method.

Next code block:
public async Task<bool> SavePlayerScore(PlayerScore data)
{
    var uri = new Uri($"{APIUri}/score");
    var json = JsonConvert.SerializeObject(data);
    var content = new StringContent(json, Encoding.UTF8,"application/json");
    var response = await ProcessPostAsync(uri, content);
    if (response.IsSuccessStatusCode)
        return true;
    return false;
}

The SavePlayerScore() method is also an asynchronous method that takes a PlayerScore object as a parameter. The first line in the method defines a new Uri object that takes the $“{APIUri}/score” as the parameter. The $ character denotes an interpolated string. You can think of it as a shorthand syntax for the string.Format method, but it’s more readable and convenient. In this case, the value of $“{APIUri}/score” will be translated to “http://localhost:56393/api/game/players/score”.

Next, we serialize the data using Newtonsoft’s JsonCovert.SerializeObject() method and pass the result into a json variable. After the data has been serialized, we then create a StringContent object with specific formatting. It then returns true for a successful response and otherwise returns false.

Next code block:
public async Task<int> GetPlayerID(string email)
{
    var uri = new Uri($"{APIUri}/{email}/");
    int id = 0;
    var response = await ProcessGetAsync(uri);
    if (response.IsSuccessStatusCode)
    {
        var content = await response.Content.ReadAsStringAsync();
        id = JsonConvert.DeserializeObject<int>(content);
    }
    return id;
}

The GetPlayerID() method is an asynchronous method that takes a string as a parameter and returns a Task of type int. Just like the previous async methods, the first line defines a new Uri object that takes an interpolated string value. The $“{APIUri}/{email}/” will be translated to something like “http://localhost:56393/api/game/players/testemail.com/”.

The next line initializes a variable called id of type int with a default value of 0. It then calls an awaitable Task called ProcessGetAsync()and passes the uri variable to it. If the response is successful, it calls another awaitable Task called ReadAsStringAsync() and assigns the result to a variable called content. It then deserializes the content value using Newtonsoft’s JsonConvert.DeserializeObject() and assigns the result to the variable called id. Finally, the value of id is returned to the method.

Next code block:
public async Task<PlayerData> GetPlayerData(string email)
{
    var uri = new Uri($"{APIUri}/profile/{email}/");
    PlayerData player = null;
    var response = await ProcessGetAsync(uri);
    if (response.IsSuccessStatusCode)
    {
        player = new PlayerData();
        var content = await response.Content.ReadAsStringAsync();
        player = JsonConvert.DeserializeObject<PlayerData>(content);
    }
    return player;
}

The preceding method is pretty much similar to the GetPlayerID() method except that it returns an object rather than an int. The GetPlayerData() method is also an asynchronous method that takes a string as a parameter and returns a Task of type PlayerData. The PlayerData is a class we defined in the DTO file that houses a few properties. The first line within the method defines a new Uri object that takes an interpolated string value. The $“{APIUri}/profile/{email}/” value will be translated to something like “http://localhost:56393/api/game/players/profile/testemail.com/”.

The next line initializes a variable called player of type PlayerData. It then calls an awaitable Task called ProcessGetAsync() and passes the uri variable to it. If the response is successful, it calls another awaitable Task called ReadAsStringAsync() and assigns the result to a variable called content. It then deserializes the content value using Newtonsoft’s JsonConvert.DeserializeObject() and assigns the result to the variable called player. If the response is not successful, then it returns the player variable with a null value to the method; otherwise, it returns the player variable with the associated data assigned from the API response.

Next code block:
private async Task<HttpResponseMessage> ProcessPostAsync(Uri uri, StringContent content)
{
    return await client.PostAsync(uri, content); ;
}
private async Task<HttpResponseMessage> ProcessGetAsync(Uri uri)
{
    return await client.GetAsync(uri);
}

The last two private methods in the preceding are used to handle common operations. The ProcessPostAsync() is an async method that takes a Uri and StringContent as parameters and returns a Task of type HttpResponseMessage. This method basically calls the PostAsync() method of the HttpClient object.

On the other hand, the ProcessGetAsync() takes only a Uri as a parameter and returns a Task of type HttpResponseMessage. This method calls the GetAsync() method of the HttpClient object.

For more information on consuming Web API from a .NET client, see the following: https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client

The PlayerManager Class

Now let’s create the class for managing the player data and score. Create a new class under the Classes folder and name it “PlayerManager.cs” and then copy the following code:
using MemoryGame.App.Helper;
using System;
using System.Threading.Tasks;
namespace MemoryGame.App.Classes
{
    public static class PlayerManager
    {
        public static void Save(PlayerProfile player)
        {
            Settings.PlayerFirstName = player.FirstName;
            Settings.PlayerLastName = player.LastName;
            Settings.PlayerEmail = player.Email;
        }
        public static PlayerProfile GetPlayerProfileFromLocal()
        {
            return new PlayerProfile
            {
                FirstName = Settings.PlayerFirstName,
                LastName = Settings.PlayerLastName,
                Email = Settings.PlayerEmail
            };
        }
        public static PlayerScore GetPlayerScoreFromLocal()
        {
            return new PlayerScore
            {
                ChallengerID = Settings.PlayerID,
                Best = Convert.ToByte(Settings.TopScore),
                DateAchieved = Settings.DateAchieved
            };
        }
        public static void UpdateBest(int score)
        {
            if (Settings.TopScore < score)
            {
                Settings.TopScore = score;
                Settings.DateAchieved = DateTime.UtcNow;
            }
        }
        public static int GetBestScore(int currentLevel)
        {
            if (Settings.TopScore > currentLevel)
                return Settings.TopScore;
            else
                return currentLevel;
        }
        public async static Task<bool> Sync()
        {
            REST.GameAPI api = new REST.GameAPI();
            bool result = false;
            try
            {
                if (!Settings.IsProfileSync)
                    result = await api.SavePlayerProfile(PlayerManager.GetPlayerProfileFromLocal(),
true);
                if (Settings.PlayerID == 0)
                    Settings.PlayerID = await api.GetPlayerID(Settings.PlayerEmail);
                result = await api.SavePlayerScore(PlayerManager.GetPlayerScoreFromLocal());
                }
                catch
                {
                    return result;
                }
            return result;
        }
        public async static Task<bool> CheckScoreAndSync(int score)
        {
            if (Settings.TopScore < score)
            {
                UpdateBest(score);
                if (Utils.IsConnectedToInternet())
                {
                    var response = await Sync();
                    return response == true ? true : false;
                }
                else
                    return false;
            }
            else
                return false;
        }
        public async static Task<PlayerData> CheckExistingPlayer(string email)
        {
            REST.GameAPI api = new REST.GameAPI();
            PlayerData player = new PlayerData();
            if (Utils.IsConnectedToInternet())
            {
                player = await api.GetPlayerData(email);
            }
            return player;
        }
    }
}

The PlayerManager class is composed of a few methods for handling data retrieval and syncing. The class and methods are marked with the keyword static , so we can directly reference them without instantiating the object. Since this class is not tied up to any object that can change the behavior of the class itself and its member, it makes more sense to use static. Notice that each method calls the method defined in the GameAPI class. We did it like this so we can separate the actual code logic for ease of maintenance and separation of concerns.

Let’s take a look at what we did there by breaking the code into sections. Let’s start with the Save() method:
public static void Save(PlayerProfile player)
{
    Settings.PlayerFirstName = player.FirstName;
    Settings.PlayerLastName = player.LastName;
    Settings.PlayerEmail = player.Email;
}

The Save() method takes a PlayerProfile object as a parameter. The PlayerProfile is an object that we define in the DTO file, which houses a few properties. The code basically stores the value from the PlayerProfile object properties to the Settings properties. In other words, this method saves the player profile such as FirstName, LastName, and Email in the device’s local storage for future use.

Next code block:
public static PlayerProfile GetPlayerProfileFromLocal()
{
    return new PlayerProfile
    {
        FirstName = Settings.PlayerFirstName,
        LastName = Settings.PlayerLastName,
        Email = Settings.PlayerEmail
    };
}

The GetPlayerProfileFromLocal() method is the opposite of the Save() method. The code fetches the player information from the local device data storage and assigns them to the PlayerProfile object.

Next code block:
public static void UpdateBest(int score)
{
    if (Settings.TopScore < score)
    {
        Settings.TopScore = score;
        Settings.DateAchieved = DateTime.UtcNow;
    }
}

As the method name suggests, the UpdateBest() method updates the challenger score. The code checks for the existing top score from the local data store and updates the TopScore property with the current score if the challenger score is greater than the existing top score.

Next code block:
public static int GetBestScore(int currentLevel)
{
    if (Settings.TopScore > currentLevel)
        return Settings.TopScore;
    else
        return currentLevel;
}

The GetBestScore() method takes an int as a parameter. The code basically compares the current score/level with the score from the local data storage and returns the highest value.

Next code block:
public async static Task<bool> Sync()
{
    REST.GameAPI api = new REST.GameAPI();
    bool result = false;
    try
    {
        if (!Settings.IsProfileSync)
            result = await api.SavePlayerProfile(PlayerManager.GetPlayerProfileFromLocal(), true);
        if (Settings.PlayerID == 0)
            Settings.PlayerID = await api.GetPlayerID(Settings.PlayerEmail);
        result = await api.SavePlayerScore(PlayerManager.GetPlayerScoreFromLocal());
    }
    catch
    {
        return result;
    }
    return result;
}

The Sync() method is an asynchronous method that returns a Task of type bool. The first line creates an instance of the GameApi object. If you remember, the GameApi class contains the code for communicating with the Web API endpoints. The next line of code initializes a bool flag in a variable called result.

Within try-block, the code checks if the challenger profile is already synced. If not, then it calls an awaitable Task from the GameApi class called SavePlayerProfile(), which takes a PlayerProfile object as the parameter and a bool parameter that indicates if the profile is new. The next if-condition checks for the existence of the challenger profile by validating the PlayerID property, which is stored in the local data storage. If the value is 0, then it calls an awaitable Task called GetPlayerID(), with an e-mail as the parameter, and assigns the result back to the Settings.PlayerID property. Otherwise, if the challenger already did a sync, it just updates the challenger score by calling the SavePlayerScore() Task.

If the code within the try-block fails, then it should go to the catch-block and return a false value to the method, indicating that the sync wasn’t successful.

Note

In real-world applications, it is recommended to handle specific exceptions and log them for debugging and easy troubleshooting.

Next code block:
public async static Task<bool> CheckScoreAndSync(int score)
{
    if (Settings.TopScore < score)
    {
        UpdateBest(score);
        if (Utils.IsConnectedToInternet())
        {
            var response = await Sync();
            return response == true ? true : false;
        }
        else
            return false;
    }
    else
        return false;
}

The CheckScoreAndSync() is also an asynchronous method that takes an int as a parameter and returns a Task of type bool. The code basically validates the score; if the current score is greater than the existing top score, then it updates the existing top score from the local data storage with the current score and ultimately calls the Sync() method.

Next code block:
public async static Task<PlayerData> CheckExistingPlayer(string email)
{
    REST.GameAPI api = new REST.GameAPI();
    PlayerData player = new PlayerData();
    if (Utils.IsConnectedToInternet())
    {
        player = await api.GetPlayerData(email);
    }
    return player;
}

The CheckExistingPlayer() is an asynchronous method that takes a string as a parameter and returns a Task of type PlayerData. This method simply calls the awaitable Task called GetPlayerData() from the GameApi class and takes an e-mail as the parameter.

Here’s a quick definition of each of these methods:
  • The Save() method saves the player information in the local device storage using the Settings plug-in.

  • The GetPlayerProfileFromLocal() method fetches the player information from the local device storage.

  • The GetPlayerScoreFromLocal() method fetches the player score details from the local device storage.

  • The UpdateBest() method updates the player score in the local device storage.

  • The GetBestScore() method fetches the player top score from the local device storage.

  • The asynchronous Sync() method syncs the player profile and score details with data from the database into the local data storage.

  • The asynchronous CheckScoreAndSync() method updates the top score to the database.

  • The asynchronous CheckExistingPlayer() method verifies the existence of a challenger from the database.

Adding the Needed Graphics and Sound File

Android

For Xamarin.Android, add the required images under the “Resources/drawable” folder. Right-click the drawable folder and then select AddExisting Item. Locate the images that you have just downloaded from the previous step and then click Add. The drawable folder should look like something in the following figure:
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig2_HTML.jpg
Figure 4-2

Adding the graphics file

To add the sound file, we need to create the “raw” folder first. Now go ahead and add a new folder under the Resources folder and name it “raw”. Add the beep.mp3 file within the folder just like in the following figure:
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig3_HTML.jpg
Figure 4-3

Adding the sound file

iOS

For Xamarin.iOS, add the required images and sound file under the “Resource” folder as shown in the following figure.
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig4_HTML.jpg
Figure 4-4

Adding the graphics and sound files

The Required XAML Pages

Before starting to create the required pages for the application, let’s talk a bit about the anatomy of the XAML file. When we created the MemogyGame.App project, a pair of files are automatically with the following names:
  • App.xaml, the XAML file; and

  • App.xaml.cs, a C# code-behind file associated with the XAML file.

If you are working with ASP.NET WebForms, you will notice that the concept of the XAML file is pretty much the same as that of the WebForm’s ASPX files. You’ll need to click the arrow next to App.xaml to see the code-behind file. Both App.xaml and App.xaml.cs contribute to a class named App that derives from Application. Most other classes with XAML files contribute to a class that derives from ContentPage; those files use XAML to define the visual contents of an entire page.

In this section, we are going the create the following XAML files that derive from the ContentPage.
  • Register

  • Home

  • Result

The Register Page

Let’s start building the Register page. Right-click the Pages folder and then select AddNew Item. On the left pane under Visual C# ItemsXamarin.Forms, select Content Page just like in the following figure:
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig5_HTML.jpg
Figure 4-5

Creating a new ContentPage file

Name the page “Register” and click Add.

Replace the default generated markup with the following:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns:="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MemoryGame.App.Pages.Register">
    <StackLayout VerticalOptions="CenterAndExpand">
        <Label Text="Working Memory Game"
               FontSize="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <Label x:Name="lblWelcome"
               Text="Register to start the fun, or Log-on to continue the challenge!"
               FontSize="Small"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <StackLayout x:Name="layoutChoose"
                     Orientation="Horizontal"
                     Spacing="5"
                     VerticalOptions="CenterAndExpand"
                     HorizontalOptions="Center">
               <Button x:Name="btnNew"
                       Text="Register"
                       FontSize="Medium"
                       HorizontalOptions="Center"
                       VerticalOptions="CenterAndExpand"
                       Clicked="OnbtnNewClicked"/>
               <Button x:Name="btnReturn"
                       Text="Log-on"
                       FontSize="Medium"
                       HorizontalOptions="Center"
                       VerticalOptions="CenterAndExpand"
                       Clicked="OnbtnReturnClicked"/>
        </StackLayout>
        <StackLayout x:Name="layoutRegister"
                     VerticalOptions="CenterAndExpand"
                     IsVisible="False">
               <Label Text="First Name" />
               <Entry x:Name="entryFirstName" />
               <Label Text="Last Name" />
               <Entry x:Name="entryLastName" />
               <Label Text="Email" />
               <Entry x:Name="entryEmail" />
               <StackLayout Orientation="Horizontal"
                            Spacing="3"
                            HorizontalOptions="Center">
                     <Button x:Name="btnRegister"
                             Text="Let's Do This!"
                             HorizontalOptions="Center"
                             VerticalOptions="CenterAndExpand"
                             Clicked="OnbtnRegisterClicked"/>
                     <Button x:Name="btnCancelRegister"
                             Text="Cancel"
                             HorizontalOptions="Center"
                             VerticalOptions="CenterAndExpand"
                             Clicked="OnbtnCancelRegisterClicked"/>
               </StackLayout>
        </StackLayout>
        <StackLayout x:Name="layoutLogin"
                     VerticalOptions="CenterAndExpand"
                     IsVisible="False">
               <Label Text="Email" />
               <Entry x:Name="entryExistingEmail" />
               <StackLayout Orientation="Horizontal" Spacing="3" HorizontalOptions="Center">
                    <Button x:Name="btnLogin"
                            Text="Let me in!"
                            HorizontalOptions="Center"
                            VerticalOptions="CenterAndExpand"
                            Clicked="OnbtnLoginClicked"/>
                    <Button x:Name="btnCancelLogin"
                            Text="Cancel"
                            HorizontalOptions="Center"
                            VerticalOptions="CenterAndExpand"
                            Clicked="OnbtnCancelLoginClicked"/>
               </StackLayout>
        </StackLayout>
    </StackLayout>
</ContentPage>

The preceding markup uses XAML to build the application UI. XAML allows you to define UIs in Xamarin.Forms applications using markup rather than code. You may have noticed that it contains some StackLayout elements to group controls in certain areas in the form. The controls are used to present the form to UI and are referred to as Button, Entry, Label, and more. Each button from the preceding markup has a Clicked event attached to it to perform a certain action in the server (a.k.a. code-behind).

For comprehensive documentation about XAML in Xamarin.Forms, see the following: https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/xaml-basics/

Now open the Register.xaml.cs file and replace the default generated code with the following code:
using MemoryGame.App.Classes;
using MemoryGame.App.Helper;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace MemoryGame.App.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
       public partial class Register : ContentPage
       {
             public Register ()
             {
                   InitializeComponent ();
             }
        enum EntryOption
        {
            Register = 0,
            Returning = 1,
            Cancel = 2
        }
        protected override void OnAppearing()
        {
            base.OnAppearing();
            NavigationPage.SetHasBackButton(this, false);
            if (!string.IsNullOrEmpty(Settings.PlayerFirstName))
                App._navPage.PushAsync(App._homePage);
        }
        async Task CheckExistingProfileAndSave(string email)
        {
             try
             {
                  PlayerData player = await PlayerManager.
                  CheckExistingPlayer(email);
                  if (string.IsNullOrEmpty(player.FirstName) && string.IsNullOrEmpty(player.LastName))
                  {
                      await App.Current.MainPage.DisplayAlert("Error", "Email does not exist.", "OK");
                  }
                  else
                  {
                      Settings.PlayerFirstName = player.FirstName.Trim();
                      Settings.PlayerLastName = player.LastName.Trim();
                      Settings.PlayerEmail = email.Trim();
                      Settings.TopScore = player.Best;
                      Settings.DateAchieved = player.DateAchieved;
                  await App._navPage.PushAsync(App._homePage);
                  }
             }
             catch
             {
                  await App.Current.MainPage.DisplayAlert("Oops", "An error occurred while connecting to the server. Please check your connection.", "OK");
             }
        }
        async Task Save()
        {
            Settings.PlayerFirstName = entryFirstName.Text.Trim();
            Settings.PlayerLastName = entryLastName.Text.Trim();
            Settings.PlayerEmail = entryEmail.Text.Trim();
            await App._navPage.PushAsync(App._homePage);
        }
        void ToggleEntryView(EntryOption option)
        {
            switch (option)
            {
                case EntryOption.Register:
                   {
                      lblWelcome.IsVisible = false;
                      layoutChoose.IsVisible = false;
                      layoutLogin.IsVisible = false;
                      layoutRegister.IsVisible = true;
                      break;
                   }
                case EntryOption.Returning:
                   {
                       lblWelcome.IsVisible = false;
                       layoutChoose.IsVisible = false;
                       layoutRegister.IsVisible = false;
                       layoutLogin.IsVisible = true;
                       break;
                       }
                case EntryOption.Cancel:
                   {
                        lblWelcome.IsVisible = true;
                         layoutChoose.IsVisible = true;
                         layoutRegister.IsVisible = false;
                         layoutLogin.IsVisible = false;
                         break;
                   }
            }
       }
       void OnbtnNewClicked(object sender, EventArgs args)
       {
            ToggleEntryView(EntryOption.Register);
       }
       void OnbtnReturnClicked(object sender, EventArgs args)
       {
            ToggleEntryView(EntryOption.Returning);
       }
       void OnbtnCancelLoginClicked(object sender, EventArgs args)
       {
            ToggleEntryView(EntryOption.Cancel);
       }
       void OnbtnCancelRegisterClicked(object sender, EventArgs args)
       {
            ToggleEntryView(EntryOption.Cancel);
       }
       async void OnbtnRegisterClicked(object sender, EventArgs args)
       {
            btnRegister.IsEnabled = false;
            if (string.IsNullOrEmpty(entryFirstName.Text)
                || string.IsNullOrEmpty(entryLastName.Text)
                || string.IsNullOrEmpty(entryEmail.Text))
                await App.Current.MainPage.DisplayAlert("Error", "Please supply the required fields.", "Got it");
            else
               await Save();
            btnRegister.IsEnabled = true;
        }
       async void OnbtnLoginClicked(object sender, EventArgs args)
       {
            if (string.IsNullOrEmpty(entryExistingEmail.Text))
                await App.Current.MainPage.DisplayAlert("Error", "Please supply your email.", "Got it");
            else
            {
                if (Utils.IsConnectedToInternet())
                {
                    btnLogin.IsEnabled = false;
                    await CheckExistingProfileAndSave(entryExistingEmail.Text);
                }
                else
                {
                    await App.Current.MainPage.DisplayAlert(“Error”, “No internet connection.”, “OK”);
                }
            }
            btnLogin.IsEnabled = true;
       }
   }
}
Let’s take a look at the code implementation details by breaking them into sections. Let’s start with this:
using MemoryGame.App.Classes;
using MemoryGame.App.Helper;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

At the very top, you’ll find a series of using keywords. This type of keyword is typically used as a directive, when it is used to create an alias for a namespace or to import types defined in other namespaces. In other words, when you want to access a certain class in your code, you need to define the namespace first.

Next code block:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Register : ContentPage
{
    public Register()
    {
        InitializeComponent();
    }
}

The Register class is a partial class that derives from a ContentPage class. In XAML, a ContentPage is a page that displays a single View, often a container like a StackLayout or ScrollView. Within the class constructor, it calls the method InitializeComponent(), which initializes a new ContentPage instance.

Next code block:
enumEntryOption
{
    Register = 0,
    Returning = 1,
    Cancel = 2
}

The preceding code is an enumeration used for toggling the buttons on the page.

Next code block:
protected override void OnAppearing()
{
    base.OnAppearing();
    NavigationPage.SetHasBackButton(this, false);
    if (!string.IsNullOrEmpty(Settings.PlayerFirstName))
        App._navPage.PushAsync(App._homePage);
}

The OnAppearing() is a built-in event of a page. This event is marked as virtual, meaning that we can override this event to customize the behavior immediately prior to the page becoming visible. In this case, we call the SetHasBackButton() method to hide the back button navigation when the Register page is loaded. The if-condition line checks the existence of the challenger’s name. If the property PlayerFirstName has a value, then it redirects the view to the Home page; otherwise, it stays in the Register page .

For more information about Xamarin.Forms navigation, see the following: https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/navigation/

Next code block:
async Task CheckExistingProfileAndSave(string email)
{
    try
    {
        PlayerData player = await PlayerManager.Check
        ExistingPlayer(email);
        if (string.IsNullOrEmpty(player.FirstName) && string.IsNullOrEmpty(player.LastName))
        {
            await App.Current.MainPage.DisplayAlert("Error", "Email does not exist.", "OK");
        }
        else
        {
            Settings.PlayerFirstName = player.FirstName.Trim();
            Settings.PlayerLastName = player.LastName.Trim();
            Settings.PlayerEmail = email.Trim();
            Settings.TopScore = player.Best;
            Settings.DateAchieved = player.DateAchieved;
            await App._navPage.PushAsync(App._homePage);
        }
    }
    catch
    {
        await App.Current.MainPage.DisplayAlert("Oops", "An error occurred while connecting to the server. Please check your connection.", "OK");
    }
}

The CheckExistingProfileAndSave() is an asynchronous method that takes a string as the parameter and returns a Task. The first line within the try-block calls the awaitable Task called CheckExistingPlayer() and assigns the result to a type of PlayerData. If the FirstName and LastName of the PlayerData object are null or empty, then it displays an error stating that the e-mail provided does not exist. Otherwise, it stores the challenger information in the local data storage via Settings properties.

Next code block:
async Task Save()
{
    Settings.PlayerFirstName = entryFirstName.Text.Trim();
    Settings.PlayerLastName = entryLastName.Text.Trim();
    Settings.PlayerEmail = entryEmail.Text.Trim();
    await App._navPage.PushAsync(App._homePage);
}

The Save() method stores the basic challenger information such as FirstName, LastName, and Email and then automatically redirects to the Home page.

Next code block:
void ToggleEntryView(EntryOption option)
{
    switch (option)
    {
         case EntryOption.Register:
             {
                 lblWelcome.IsVisible = false;
                 layoutChoose.IsVisible = false;
                 layoutLogin.IsVisible = false;
                 layoutRegister.IsVisible = true;
                 break;
             }
        case EntryOption.Returning:
             {
                 lblWelcome.IsVisible = false;
                 layoutChoose.IsVisible = false;
                 layoutRegister.IsVisible = false;
                 layoutLogin.IsVisible = true;
                 break;
              }
        case EntryOption.Cancel:
             {
                 lblWelcome.IsVisible = true;
                 layoutChoose.IsVisible = true;
                 layoutRegister.IsVisible = false;
                 layoutLogin.IsVisible = false;
                 break;
             }
    }
}

The ToggleEntryView() method takes an EntryOption enumeration as a parameter. This method basically handles the switching of register and login container layout in the Register page.

Next code block:
void OnbtnNewClicked(object sender, EventArgs args)
{
    ToggleEntryView(EntryOption.Register);
}
void OnbtnReturnClicked(object sender, EventArgs args)
{
    ToggleEntryView(EntryOption.Returning);
}
void OnbtnCancelLoginClicked(object sender, EventArgs args)
{
    ToggleEntryView(EntryOption.Cancel);
}
void OnbtnCancelRegisterClicked(object sender, EventArgs args)
{
    ToggleEntryView(EntryOption.Cancel);
}

The preceding code comprises event handlers for buttons that invoke the ToggleEntryView() method . The OnbtnNewClicked event shows the Register view with a cancel button. The OnbtnReturnClicked event, on the other hand, shows the Login view with a cancel button. The remaining events are used to revert the view to original state.

Next code block:
async void OnbtnRegisterClicked(object sender, EventArgs args)
{
    btnRegister.IsEnabled = false;
    if (string.IsNullOrEmpty(entryFirstName.Text)
        || string.IsNullOrEmpty(entryLastName.Text)
        || string.IsNullOrEmpty(entryEmail.Text))
        await App.Current.MainPage.DisplayAlert("Error", "Please supply the required fields.", "Got it");
    else
        await Save();
    btnRegister.IsEnabled = true;
}

The OnbtnRegisterClicked is an asynchronous event that returns void. As you may know, async methods can return Task<T>, Task, or void. In almost all cases, you want to return Task<T> or Task, and return void only when you have to. Returning void for async event handlers is great, as we can perform asynchronous operations without blocking the UI thread.

The first line of the code within the event disables the button btnRegister and then performs some asynchronous operations. If the FirstName, LastName, and Email fields are left empty, then it shows an error. Otherwise, it calls the Save() method.

Next code block:
async void OnbtnLoginClicked(object sender, EventArgs args)
{
    if (string.IsNullOrEmpty(entryExistingEmail.Text))
        await App.Current.MainPage.DisplayAlert("Error", "Please supply your email.", "Got it");
    else
    {
        if (Utils.IsConnectedToInternet())
        {
            btnLogin.IsEnabled = false;
            await CheckExistingProfileAndSave(entryExistingEmail.Text);
        }
        else
        {
            await App.Current.MainPage.DisplayAlert("Error", "No internet connection.", "OK");
        }
    }
    btnLogin.IsEnabled = true;
}

The OnbtnLoginClicked event is also an asynchronous event that returns void. This event is where the user credential is validated: in this case, the e-mail address value. The first line of code within the event handler checks for the e-mail address value. If it’s empty, then it displays an error; otherwise, it saves the challenger information to the local data storage by calling the CheckExistingProfileAndSave() method.

The Home Page

Add a new Content Page under the Pages folder and name it “Home”. Replace the default generated code with the following code:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns:="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MemoryGame.App.Pages.Home">
    <StackLayout Padding="2">
        <StackLayout>
            <StackLayout Orientation="Horizontal">
                <Label x:Name="lblBest"
                       FontSize="Medium"
                       HorizontalOptions="StartAndExpand" />
                <Button x:Name="btnSync"
                        Text="Sync"
                        Clicked="OnbtnSyncClicked"
                        HorizontalOptions="EndAndExpand"
                        VerticalOptions="CenterAndExpand" />
                <Button x:Name="btnLogOut"
                        Text="Logout"
                        Clicked="OnbtnLogoutClicked"
                        HorizontalOptions="EndAndExpand"
                        VerticalOptions="CenterAndExpand" />
            </StackLayout>
            <Label x:Name="lblTime"
                   FontSize="Large"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
        </StackLayout>
        <Label x:Name="lblLevel"
               FontSize="Small"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <StackLayout Orientation="Horizontal"
                     Spacing="2"
                     HorizontalOptions="Center"
                     BackgroundColor="White">
              <Image x:Name="imgLightOff"
                     Source="lightoff.png"
                     WidthRequest="60"
                     HeightRequest="20" />
              <Image x:Name="imgLightOff2"
                     Source="lightoff.png"
                     IsVisible="False"
                     WidthRequest="60"
                     HeightRequest="20" />
              <Image x:Name="imgLightOn"
                     Source="lighton.png"
                     IsVisible="False"
                     WidthRequest="60"
                     HeightRequest="20" />
              <Image x:Name="imgSpeaker"
                     Source="speakeron.png"
                     WidthRequest="60"
                     HeightRequest="40" />
              <Image x:Name="imgHaptic"
                     Source="vibration.png"
                     WidthRequest="60"
                     HeightRequest="20" />
        </StackLayout>
        <Label Text="The light will blink on, the speaker will beep and the device will vibrate at different times. Try to count how many times each one happens ."
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <Button x:Name="btnStart"
                Text="Start"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                Clicked="OnButtonClicked"/>
    </StackLayout>
</ContentPage>

The preceding XAML markup contains three Label, three Button, and five Image. The Label elements are used for displaying the existing saved top score, the current top score, and the instructions to play the game. The Button elements are used for syncing data to the database, logging out, and starting the game. The Image elements are used for displaying a bulb (on and off), speaker, and haptic indication.

Open the Home.xaml.cs file and replace the default generated code with the following code:
using MemoryGame.App.Classes;
using MemoryGame.App.Helper;
using MemoryGame.App.Services;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace MemoryGame.App.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class Home : ContentPage
    {
        public Home()
        {
            InitializeComponent();
        }
        enum PlayType
        {
            Blink = 0,
            Sound = 1,
            Haptic = 2
        }
        private int _cycleStartInMS = 0;
        private int _cycleMaxInMS = 10000;
        private const int _cycleIntervalInMS = 2000;
        private const int _eventTypeCount = 3;
        public statici nt CurrentGameBlinkCount { get; private set; } = 0;
        public static int CurrentGameSoundCount { get; private set; } = 0;
        public static int CurrentGameHapticCount { get; private set; } = 0;
        public static int CurrentGameLevel { get; private set; } = 1;
        protected override void OnAppearing()
        {
            base.OnAppearing();
            NavigationPage.SetHasBackButton(this, false);
            PlayerManager.UpdateBest(CurrentGameLevel);
            if (Result._answered)
                LevelUp();
            else
                ResetLevel();
            lblBest.Text = $"Best: Level {PlayerManager.GetBestScore(CurrentGameLevel)}";
            lblLevel.Text = $"Level {CurrentGameLevel}";
        }
        static void IncrementPlayCount(PlayType play)
        {
           switch (play)
           {
               case PlayType.Blink:
                  {
                       CurrentGameBlinkCount++;
                       break;
                  }
               case PlayType.Sound:
                  {
                       CurrentGameSoundCount++;
                       break;
                  }
               case PlayType.Haptic:
                  {
                       CurrentGameHapticCount++;
                       break;
                  }
            }
        }
        public static void IncrementGameLevel()
        {
            CurrentGameLevel++;
        }
        void ResetLevel()
        {
            CurrentGameLevel = 1;
            _cycleStartInMS = _cycleIntervalInMS;
            lblTime.Text = string.Empty;
            btnStart.Text = "Start";
            btnStart.IsEnabled = true;
        }
        async void StartRandomPlay()
        {
            await Task.Run(() =>
            {
                Random rnd = new Random(Guid.NewGuid().GetHashCode());
                int choice = rnd.Next(0, _eventTypeCount);
                switch (choice)
                {
                    case (int)PlayType.Blink:
                        {
                           Device.BeginInvokeOnMainThread(async () =>
                            {
                              await imgLightOff.FadeTo(0, 200);
                              imgLightOff2.IsVisible = false;
                              imgLightOff.IsVisible = true;
                              imgLightOff.Source = ImageSource.FromFile("lighton.png");
                              await imgLightOff.FadeTo(1, 200);
                            });
                            IncrementPlayCount(PlayType.Blink);
                            break;
                        }
                    case (int)PlayType.Sound:
                        {
                            DependencyService.Get<ISound>().PlayMp3File("beep.mp3");
                            IncrementPlayCount(PlayType.Sound);
                            break;
                        }
                    case (int)PlayType.Haptic:
                        {
                            DependencyService.Get<IHaptic>().ActivateHaptic();
                            IncrementPlayCount(PlayType.Haptic);
                            break;
                        }
                }
            });
        }
        void ResetGameCount()
        {
            CurrentGameBlinkCount = 0;
            CurrentGameSoundCount = 0;
            CurrentGameHapticCount = 0;
        }
        void LevelUp()
        {
            _cycleStartInMS = _cycleStartInMS - 200;
            //minus 200 ms
        }
        void Play()
        {
            int timeLapsed = 0;
            int duration = 0;
            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
            {
                duration++;
                lblTime.Text = $"Timer: { TimeSpan.FromSeconds(duration).ToString("ss")}";
                if (duration < 10)
                    return true;
                else
                    return false;
            });
            Device.StartTimer(TimeSpan.FromMilliseconds(_cycleStartInMS), () => {
                timeLapsed = timeLapsed + _cycleStartInMS;
                Device.BeginInvokeOnMainThread(async () =>
                {
                    imgLightOff2.IsVisible = true;
                    imgLightOff.IsVisible = false;
                    await Task.Delay(200);
                });
                if (timeLapsed <= _cycleMaxInMS)
                {
                    StartRandomPlay();
                    return true; //continue
                }
                App._navPage.PushAsync(App._resultPage);
                    return false; //don’t continue
            });
        }
        void OnButtonClicked(object sender, EventArgs args)
        {
            btnStart.Text = "Game Started...";
            btnStart.IsEnabled = false;
            ResetGameCount();
            Play();
        }
        async void OnbtnSyncClicked(object sender, EventArgs args)
        {
            if (Utils.IsConnectedToInternet())
            {
                btnSync.Text = "Syncing...";
                btnSync.IsEnabled = false;
                btnStart.IsEnabled = false;
                var response = await PlayerManager.Sync();
                if (!response)
                    await App.Current.MainPage.DisplayAlert("Oops", "An error occurred while connecting to the server. Please check your connection.", "OK");
                else
                    await App.Current.MainPage.DisplayAlert("Sync", "Data synced!","OK");
                btnSync.Text = "Sync";
                btnSync.IsEnabled = true;
                btnStart.IsEnabled = true;
            }
            else
            {
                await App.Current.MainPage.DisplayAlert("Error", "No internet connection.", "OK");
            }
        }
        async void OnbtnLogoutClicked(object sender, EventArgs args)
        {
            if (Utils.IsConnectedToInternet())
            {
                btnLogOut.IsEnabled = false;
                var response = await PlayerManager.Sync();
                if (response)
                {
                    Settings.ClearEverything();
                    await App._navPage.PopToRootAsync();
                }
                else
                    await App.Current.MainPage.DisplayAlert("Oops","An error occurred while connecting to the server. Please check your connection.", "OK");
            }
            else
                    await App.Current.MainPage.DisplayAlert("Oops", "No internet connection. Please check your network.", "OK");
            btnLogOut.IsEnabled = true;
        }
    }
}

The code-behind for the Home page is expected to be long, because this is where the game logic is handled. I keep it this way in order for you to easily reference the relevant code logic in one place and for simplicity’s sake. In a real-world scenario, you may want to break the code into classes and identify components that can be reusable.

Let’s see what the code does by breaking it into sections. Let’s start with the class-level definition:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Home : ContentPage
{
    public Home()
    {
        InitializeComponent();
    }
}

Just like any other XAML page, the Home class inherits the ContentPage class. Within the class constructor, it calls the method InitializeComponent() to initialize a new ContentPage instance.

Next code block:
enum PlayType
{
    Blink = 0,
    Sound = 1,
    Haptic = 2
}

The PlayType is an enum that consists of three main entries: Blink, Sound, and Haptic. This enum will be used later in the code to identify the type of event played.

Next code block:
private int _cycleStartInMS = 0;
private int _cycleMaxInMS = 10000;
private const int _cycleIntervalInMS = 2000;
private const int _eventTypeCount = 3;

The preceding code comprises the private global variables of type int that will be used within the class. The _cycleStartInMS variable value is expressed in milliseconds and defaults to 0. This variable indicates the time when the app should trigger a new cycle to start the play. The cycleMaxInMS variable indicates the maximum time to when the app stops the play. The default value is 10,000 milliseconds, or 10 seconds. The last two variables are marked as const, meaning the value assigned to them won’t change. The _cycleIntervalInMS variable indicates the time interval between playing different event types such as blinking an image, playing a sound, or activating vibration on the device. The interval value is 2000 milliseconds, equivalent to 2 seconds. The eventTypeCount variable indicates the number of event types, for which the value in this case is 3.

Next code block:
public static int CurrentGameBlinkCount { get; privateset; } = 0;
public static int CurrentGameSoundCount { get; privateset; } = 0;
public static int CurrentGameHapticCount { get; privateset; } = 0;
public static int CurrentGameLevel { get; privateset; } = 1;

The preceding code comprises the public properties for the class. They are marked public and static, so other class can access them without having to create an instance of the Home class. The preceding syntax uses property initializers, which was introduced in C# 6.0.

The CurrentGameBlinkCount property holds the number of blink counts with the default value of 0. The CurrentGameSoundCount property holds the number of sound counts with the default value of 0. The CurrentGameHapticCount property holds the number of haptic counts with the default value of 0. Last but not least, the CurrentGameLevel holds the level/score value.

Next code block:
protected override void OnAppearing()
{
    base.OnAppearing();
    NavigationPage.SetHasBackButton(this, false);
    PlayerManager.UpdateBest(CurrentGameLevel);
    if (Result._answered)
        LevelUp();
    else
        ResetLevel();
    lblBest.Text = $"Best: Level {PlayerManager.GetBestScore(CurrentGameLevel)}";
    lblLevel.Text = $"Level {CurrentGameLevel}";
}

The OnAppearing() method fires before the page gets visible. The preceding code disables the back navigation of the app and then updates the challenger top score. The if-condition checks the value of _answered from the Result page. If true, then it calls the LevelUp() method, otherwise it calls the ResetLevel().

The last two lines of code within the method sets the label’s Text property to display the top score and current score.

Next code block:
static void IncrementPlayCount(PlayType play)
{
    switch (play)
    {
        case PlayType.Blink:
        {
            CurrentGameBlinkCount++;
            break;
        }
        case PlayType.Sound:
            {
                CurrentGameSoundCount++;
                break;
            }
        case PlayType.Haptic:
            {
                CurrentGameHapticCount++;
                break;
            }
    }
}

The IncrementPlayCount() method takes a PlayType object as a parameter. This method basically increases the number of each event type based on the enum value.

Next code block:
public static void IncrementGameLevel()
{
    CurrentGameLevel++;
}

The IncrementGameLevel() increases the level/score value. This method is marked as public so other class can invoke it.

Next code block:
void ResetLevel()
{
    CurrentGameLevel = 1;
    _cycleStartInMS = _cycleIntervalInMS;
    lblTime.Text = string.Empty;
}

The ResetLevel() method resets the level/score and play cycle time and clears the time displayed in the view.

Next code block:
async void StartRandomPlay()
{
    await Task.Run(() =>
    {
        Random rnd = new Random(Guid.NewGuid().GetHashCode());
        int choice = rnd.Next(0, _eventTypeCount);
        switch (choice)
        {
            case (int)PlayType.Blink:
                {
                     Device.BeginInvokeOnMainThread(async () =>
                     {
                           await imgLightOff.FadeTo(0, 200);
                           imgLightOff2.IsVisible = false;
                           imgLightOff.IsVisible = true;
                           imgLightOff.Source = ImageSource.FromFile("lighton.png");
                           await imgLightOff.FadeTo(1, 200);
                     });
                     IncrementPlayCount(PlayType.Blink);
                     break;
                }
            case (int)PlayType.Sound:
                {
                     DependencyService.Get<ISound>().PlayMp3File("beep.mp3");
                     IncrementPlayCount(PlayType.Sound);
                     break;
                }
            case (int)PlayType.Haptic:
                {
                     DependencyService.Get<IHaptic>().ActivateHaptic();
                     IncrementPlayCount(PlayType.Haptic);
                     break;
                }
        }
    });
}

The StartRandomPlay() is an asynchronous method that returns a void. The preceding code is the core method of the Home class. The method is responsible for activating different criteria on a random basis, whether invoking a sound, making a vibration, or just blinking an image. Notice that we’ve used the DependencyService class to inject the interface that we’ve defined in previous section of this Chapter. This allows us to perform platform specific implementations for playing a sound or activating a device vibration.

Next code block:
void ResetGameCount()
{
    CurrentGameBlinkCount = 0;
    CurrentGameSoundCount = 0;
    CurrentGameHapticCount = 0;
}

The preceding code simply resets the properties value to 0.

Next code block:
void LevelUp()
{
    _cycleStartInMS = _cycleStartInMS - 200; //minus 200 ms
}

The preceding code decreases the cycle interval for triggering a new random event. In other words, the 2-second cycle will be decreased by 200 ms per level. This is where the game gets exciting, because the higher your level/score goes, the faster the different event types are triggered until you can’t remember which type of event has occurred.

Next code block:
void Play()
{
    int timeLapsed = 0;
    int duration = 0;
    Device.StartTimer(TimeSpan.FromSeconds(1), () =>
    {
        duration++;
        lblTime.Text = $"Timer: { TimeSpan.FromSeconds(duration).ToString("ss")}";
        if (duration < 10)
            return true;
        else
            return false;
        });
        Device.StartTimer(TimeSpan.FromMilliseconds(_cycleStartInMS), () => {
            timeLapsed = timeLapsed + _cycleStartInMS;
            Device.BeginInvokeOnMainThread(async () =>
            {
                imgLightOff2.IsVisible = true;
                imgLightOff.IsVisible = false;
                await Task.Delay(200);
            });
            if (timeLapsed <= _cycleMaxInMS)
            {
                StartRandomPlay();
                return true; //continue
            }
            App._navPage.PushAsync(App._resultPage);
            return false; //don’t continue
        });
}

The preceding code invokes two methods for starting a timer on the view. The Device.StartTimer() starts a recurring timer on the UI thread using the device clock capabilities. The first one creates a countdown timer in the view starting from 10 seconds to 0 and displays the result to a Label element in real time. The second invokation of the Device.StartTimer() method is responsible for triggering a new random event based in the current value of the _cycleMaxInMS value.

Next code block:
void OnButtonClicked(object sender, EventArgs args)
{
    btnStart.Text = "Game Started...";
    btnStart.IsEnabled = false;
    ResetGameCount();
    Play();
}

The OnButtonClicked event activates and starts the game by calling the Play() method.

Next code block:
async void OnbtnSyncClicked(object sender, EventArgs args)
{
    if (Utils.IsConnectedToInternet())
    {
        btnSync.Text = "Syncing...";
        btnSync.IsEnabled = false;
        btnStart.IsEnabled = false;
        var response = await PlayerManager.Sync();
        if (!response)
            await App.Current.MainPage                  DisplayAlert("Oops"                  "An error occurred while connecting to the server. Please check your connection.", "OK");
        else
            await App.Current.MainPage.DisplayAlert("Sync", "Data synced!","OK");
        btnSync.Text = "Sync";
        btnSync.IsEnabled = true;
        btnStart.IsEnabled = true;
    }
    else
    {
        await App.Current.MainPage                  DisplayAlert("Error"                  "No internet connection."                  "OK");
    }
}

The OnbtnSyncClicked() is an asynchronous event handler that syncs data to the database. The first line of the code within the method checks for the connection using the Utils.IsConnectedToInternet() method. If the device is connected to an Internet or wifi, then it enables data sync by calling the awaitable Sync() method from the PlayerManager class.

Next code block:
async void OnbtnLogoutClicked(object sender, EventArgs args)
{
    if (Utils.IsConnectedToInternet())
    {
        btnLogOut.IsEnabled = false;
        var response = await PlayerManager.Sync();
        if (response)
        {
            Settings.ClearEverything();
            await App._navPage.PopToRootAsync();
        }
        else
            await App.Current.MainPage                  DisplayAlert("Oops"                  "An error occurred while connecting to the server. Please check your connection."                  "OK");
    }
    else
        await App.Current.MainPage                  DisplayAlert("Oops"                  "No internet connection. Please check your network."                  "OK");
    btnLogOut.IsEnabled = true;
}

The OnbtnLogoutClicked event handles the logout functionality of the application. Just like the sync feature, it first checks for Internet connectivity. If the device is connected, it will then invoke the Sync() method to persist the data in the database. If it syncs successfully, then it clears the data from the local device storage using the Settings.ClearEverything() method and redirects the user back to the default page.

The Result Page

Add a new Content Page under the Pages folder and name it “Result”. Replace the default generated code with the following code:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns:="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MemoryGame.App.Pages.Result">
    <StackLayout>
        <Label Text="How many times did the light blink, the speaker beep and the device vibrate?"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
        <StackLayout Orientation="Horizontal"
             Spacing="2"
             HorizontalOptions="Center"
             BackgroundColor="White">
            <Image x:Name="imgLight"
                   Source="lightoff.png"
                   WidthRequest="60"
                   HeightRequest="20" />
            <Image x:Name="imgSpeaker"
                   Source="speakeron.png"
                   WidthRequest="60"
                   HeightRequest="20" />
            <Image x:Name="imgHaptic"
                   Source="vibration.png"
                   WidthRequest="60"
                   HeightRequest="20" />
        </StackLayout>
        <StackLayout Orientation="Horizontal"
                     HorizontalOptions="Center"
                     Spacing="5">
            <Picker x:Name="pickerLight"
                    HorizontalOptions="FillAndExpand"
                    WidthRequest="100">
                <Picker.Items>
                    <x:String>0</x:String>
                    <x:String>1</x:String>
                    <x:String>2</x:String>
                    <x:String>3</x:String>
                    <x:String>4</x:String>
                    <x:String>5</x:String>
                    <x:String>6</x:String>
                    <x:String>7</x:String>
                    <x:String>8</x:String>
                    <x:String>9</x:String>
                    <x:String>10</x:String>
                </Picker.Items>
            </Picker>
            <Picker x:Name="pickerSpeaker"
                    HorizontalOptions="FillAndExpand"
                    WidthRequest="100">
                <Picker.Items>
                    <x:String>0</x:String>
                    <x:String>1</x:String>
                    <x:String>2</x:String>
                    <x:String>3</x:String>
                    <x:String>4</x:String>
                    <x:String>5</x:String>
                    <x:String>6</x:String>
<x:String>7</x:String>
<x:String>8</x:String>
<x:String>9</x:String>
<x:String>10</x:String>
                </Picker.Items>
            </Picker>
            <Picker x:Name="pickerHaptic"
                    HorizontalOptions="FillAndExpand"
                    WidthRequest="100">
                <Picker.Items>
<x:String>0</x:String>
<x:String>1</x:String>
<x:String>2</x:String>
<x:String>3</x:String>
<x:String>4</x:String>
<x:String>5</x:String>
<x:String>6</x:String>
<x:String>7</x:String>
<x:String>8</x:String>
<x:String>9</x:String>
<x:String>10</x:String>
</Picker.Items>
</Picker>
        </StackLayout>
        <Label x:Name="lblText"
                FontSize="20"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand" />
        <StackLayout Orientation="Horizontal"
                     HorizontalOptions="Center"
                     Spacing="40">
            <Label x:Name="lblBlinkCount"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
            <Label x:Name="lblBeepCount"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
            <Label x:Name="lblHapticCount"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
        </StackLayout>
        <Button x:Name="btnSubmit"
                Text="Submit"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                Clicked="OnButtonClicked"/>
        <Button x:Name="btnRetry"
                Text="Retry"
                IsVisible="False"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                Clicked="OnRetryButtonClicked"/>
    </StackLayout>
</ContentPage>

The preceding XAML markup contains a few Label, Button, Picker and Image elements. The Picker elements are used for storing a list of items for a challenger to pick. The Label elements are used for displaying the answer count for each event type that has occurred. The Button elements are used for submitting the answers or navigating back to the Home page to restart the game. The Image elements are used for displaying a bulb, a speaker, and a haptic indication.

Open the Result.xaml.cs file and replace the default generated code with the following code:
using MemoryGame.App.Classes;
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace MemoryGame.App.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
        public partial class Result : ContentPage
        {
          public static bool _answered = false;
          public Result()
          {
              InitializeComponent();
              ClearResult();
          }
          protected override void OnAppearing()
          {
              base.OnAppearing();
              ClearResult();
              NavigationPage.SetHasBackButton(this, false);
          }
          void ClearResult()
          {
              lblText.Text = string.Empty;
              lblBlinkCount.Text = string.Empty;
              lblBeepCount.Text = string.Empty;
              lblHapticCount.Text = string.Empty;
              pickerLight.SelectedIndex = 0;
              pickerSpeaker.SelectedIndex = 0;
              pickerHaptic.SelectedIndex = 0;
              btnSubmit.IsVisible = true;
              btnRetry.IsVisible = false;
              _answered = false;
          }
          bool CheckAnswer(int actualAnswer, int selectedAnswer)
          {
              if (selectedAnswer == actualAnswer)
                  return true;
              else
                  return false;
          }
          void Retry()
          {
              btnSubmit.IsVisible = false;
              btnRetry.IsVisible = true;
          }
          async void OnButtonClicked(object sender, EventArgs args)
          {
              if (pickerLight.SelectedIndex >= 0 && pickerSpeaker.SelectedIndex >= 0 && pickerHaptic.SelectedIndex >= 0)
              {
                  lblText.Text = "The actual answers are:";
                  lblBlinkCount.Text = Home.CurrentGameBlinkCount.ToString();
                  lblBeepCount.Text = Home.CurrentGameSoundCount.ToString();
                  lblHapticCount.Text = Home.CurrentGameHapticCount.ToString();
                  int blinkCountAnswer = Convert.ToInt32(pickerLight.Items[pickerLight.SelectedIndex]);
                  int soundCountAnswer = Convert.ToInt32(pickerSpeaker.Items[pickerSpeaker.SelectedIndex]);
                  int hapticCountAnswer = Convert.ToInt32(pickerHaptic.Items[pickerHaptic.SelectedIndex]);
                  if (CheckAnswer(Home.CurrentGameBlinkCount, blinkCountAnswer))
                      if (CheckAnswer(Home.CurrentGameSoundCount, soundCountAnswer))
                          if (CheckAnswer(Home.CurrentGameHapticCount,hapticCountAnswer))
                          {
                              _answered = true;
                              Home.IncrementGameLevel();
                              var isSynced = PlayerManager.CheckScoreAndSync(Home.CurrentGameLevel);
                              var answer = await App.Current.MainPage.DisplayAlert("Congrats!", $"You’ve got it all right and made it to level {Home.CurrentGameLevel}. Continue?", "Yes", "No");
                              if (answer)
                              await App._navPage.PopAsync();
                          else
                              Retry();
                      }
                  if (!_answered)
                  {
                      var isSynced = PlayerManager.CheckScoreAndSync(Home.CurrentGameLevel);
                      var answer = await App.Current.MainPage.DisplayAlert("Game Over!", $"Your current best is at level{Home.CurrentGameLevel}. Retry?", "Yes", "No");
                      if (answer)
                          await App._navPage.PopAsync();
                      else
                          Retry();
                  }
              }
          }
          void OnRetryButtonClicked(object sender, EventArgs args)
          {
              App._navPage.PopAsync();
          }
    }
}

The preceding code handles the logic for validating the answers against the actual count of each event type occurred. If all answers are correct, then it will prompt you with a message asking if you want to proceed to the next level or not.

Setting the Page Navigation

Now that we have the required pages set up, let’s declare them on the App class to create a simple navigation with a default page.

Go ahead and open the App.xaml.cs file and replace the existing code with the following code:
using MemoryGame.App.Pages;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace MemoryGame.App
{
    public partial class App : Application
    {
        public static NavigationPage _navPage;
        public static Home _homePage;
        public static Result _resultPage;
        public static Register _registerPage;
        public App()
        {
            InitializeComponent();
            _homePage = new Home();
            _resultPage = new Result();
            _registerPage = new Register();
            _navPage = new NavigationPage(_registerPage);
            MainPage = _navPage;
        }
        protected override void OnStart()
        {
            // Handle when your app starts
        }
        protected override void OnSleep()
        {
            // Handle when your app sleeps
        }
        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}
The App class inherits the Application base class, which offers the following features:
  • A MainPage property, which is where to set the initial page for the app.

  • A persistent Properties dictionary to store simple values across lifecycle state changes.

  • A static Current property that contains a reference to the current application object.

The code within the app class defines a public static NavigationPage object and the three Pages that we’ve created in the previous section: Register, Home, and Result. These objects are then initialized in the class contractor with the default page set to the Register page. The MainPage property on the application class sets the root page of the application.

For more information about the Xamarin.Forms app class, see the following: https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/application-class

Summary of Files Added

Here’s what the MemoryGame.App project looks like after all the files are added.
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig6_HTML.jpg
Figure 4-6

Summary of newly added files

Implementing the Haptic and Sound Services

Now it’s time for us to provide an actual implementation of each interface created in previous sections of this chapter. Let’s start with the Xamain.Android. Add a new folder called “Services” in the MemoryGame.App.Android project and then create the following classes:
  • HapticServer.cs

  • SoundService.cs

Xamarin.Android Haptic Service

Open HapticService.cs file and replace the default generated code with the following code:
using Android.Content;
using Android.OS;
using Xamarin.Forms;
using MemoryGame.App.Droid.Services;
using MemoryGame.App.Services;
[assembly: Dependency(typeof(HapticService))]
namespace MemoryGame.App.Droid.Services
{
    public class HapticService : IHaptic
    {
        public HapticService() { }
        public void ActivateHaptic()
        {
            VibrationEffect effect = VibrationEffect.CreateOneShot(100, VibrationEffect.DefaultAmplitude);
            Vibrator vibrator = (Vibrator)global::Android.App.Application.Context.GetSystemService (Context.VibratorService);
            vibrator.Vibrate(effect);
        }
    }
}

The HapticService class implements the ActivateHaptic() method of the IHaptic interface. The preceding code contains Android-specific implementation for activating the device vibration.

Xamarin.Android Sound Service

Open SoundService.cs file and replace the default generated code with the following code:
using Xamarin.Forms;
using Android.Media;
using MemoryGame.App.Droid.Services;
using MemoryGame.App.Services;
[assembly: Dependency(typeof(SoundService))]
namespace MemoryGame.App.Droid.Services
{
    public class SoundService : ISound
    {
        public SoundService() { }
        private MediaPlayer _mediaPlayer;
        public bool PlayMp3File(string fileName)
        {
            _mediaPlayer = MediaPlayer.Create(Android.App.
            Application.Context, Resource.Raw.beep);
            _mediaPlayer.Start();
            return true;
        }
        public bool PlayWavFile(string fileName)
        {
            //TO DO: Own implementation here
            return true;
        }
    }
}

The SoundService class implements the PlayMp3File() method of the ISound interface. The preceding code contains Android-specific implementation for playing a media.

Now switch to the MemoryGame.App.iOS project. Add a new folder called “Services” and then create the following classes:
  • HapticServer.cs

  • SoundService.cs

Xamarin.iOS Haptic Service

Open the HapticService.cs file and replace the default generated code with the following code:
using Xamarin.Forms;
using AudioToolbox;
using MemoryGame.App.iOS.Services;
using MemoryGame.App.Services;
[assembly: Dependency(typeof(HapticService))]
namespace MemoryGame.App.iOS.Services
{
    public class HapticService : IHaptic
    {
        public HapticService() { }
        public void ActivateHaptic()
        {
            SystemSound.Vibrate.PlaySystemSound();
        }
    }
}

The preceding code contains iOS-specific implementation for activating device vibration. It uses the SystemSound.Vibrate.PlaySystemSound() to vibrate the device in iOS.

Xamarin.iOS Sound Service

Open the SoundService.cs file and replace the default generated code with the following code:
using Xamarin.Forms;
using MemoryGame.App.iOS.Services;
using System.IO;
using Foundation;
using AVFoundation;
using MemoryGame.App.Services;
[assembly: Dependency(typeof(SoundService))]
namespace MemoryGame.App.iOS.Services
{
    public class SoundService : NSObject, ISound, IAVAudioPlayerDelegate
    {
        public SoundService(){}
        public bool PlayWavFile(string fileName)
        {
            return true;
        }
        public bool PlayMp3File(string fileName)
        {
            var played = false;
            NSError error = null;
            AVAudioSession.SharedInstance().SetCategory(AVAudioSession.CategoryPlayback, out error);
            string sFilePath = NSBundle.MainBundle.PathForResource(Path.GetFileNameWithoutExtension(fileName), "mp3");
            var url = NSUrl.FromString(sFilePath);
            var _player = AVAudioPlayer.FromUrl(url);
            _player.Delegate = this;
            _player.Volume = 100f;
            played = _player.PrepareToPlay();
            _player.FinishedPlaying += (object sender, AVStatusEventArgs e) => {
            _player = null;
            };
            played = _player.Play();
            return played;
        }
    }
}

The SoundService class implements the PlayMp3File() method of the ISound interface. The preceding code contains iOS-specific implementation for playing a media.

Note

For iOS, add the required images and sound file under the Resource folder.

Setting Permissions

Xamarin.Android

For Android, open the AndroidManifest.xml file as shown in the following figure:
../images/475615_1_En_4_Chapter/475615_1_En_4_Fig7_HTML.jpg
Figure 4-7

The AndroidManifest.xml file

Then, add the following configuration:
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

Xamarin.iOS

An iOS device doesn’t require any permissions. In Android, the system seeks the user’s permission while the app is being installed. But iOS allows you to go ahead with an installation, seeking permission when the user is using a feature that requires specific permission.

Note

Apple has made several enhancements to both security and privacy in iOS 10 (and greater) that will help the developer improve the security of their apps and ensure the end user’s privacy. For the new iOS 10 Privacy Permission Settings, see: https://blog.xamarin.com/new-ios-10-privacy-permission-settings/

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.255.134