Previous part: Reusable Components and Styles in MAUI MVVM .NET 9 [GamesCatalog] - Part 3
Step 1. Let's create an account and register the application.
![Create an account]()
https://api-docs.igdb.com/#account-creation
This way, we have the keys to generate a temporary token.
![Temporary token]()
https://api-docs.igdb.com/#account-creation
![Postman Print Token]()
The token is valid for 2 months, so for now, we won't need a function to update it constantly.
Step 2. Let's create the logic layers, the models, and the API access layer.
![API access layer]()
Step 3. Let's create a model to receive API responses in a general way.
![API responses]()
Code
public class ApiResp
{
public bool Success { get; set; }
public string? Content { get; set; }
public ErrorTypes? Error { get; set; }
public bool TryRefreshToken { get; set; }
}
public enum ErrorTypes
{
TokenExpired = 0,
Unknown = 1,
ServerUnavaliable = 2,
WrongEmailOrPassword = 3,
Unauthorized = 4,
BodyContentNull = 5,
}
Step 4. Let's create the class that will store our private API keys. While we don't have a local database, we'll keep a fixed token.
![Private API keys]()
Code
public static class ApiKeys
{
public const string TOKENTEMP = <Token>;
public const string CLIENTID = <clientId>;
}
Step 5. Let's create the class that will access game data using our Client-ID and the token we generated in the POST request, which is currently limited to 20 records.
![Client-ID]()
Code
using Models.Resps;
using System.Net;
using System.Text;
namespace ApiRepo
{
public static class IGDBGamesAPIRepo
{
public async static Task<ApiResp> Get(string search, int startIndex)
{
try
{
HttpClient httpClient = new();
httpClient.DefaultRequestHeaders.Add("Client-ID", ApiKeys.CLIENTID);
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKeys.TOKENTEMP}");
var bodyContent = new StringContent(
$"fields cover,cover.url,cover.image_id,first_release_date,name,platforms.abbreviation;" +
$"search \"{search}\"; limit 20; offset {startIndex};",
Encoding.UTF8,
"application/json"
);
HttpResponseMessage httpResponse = await httpClient.PostAsync(
"https://api.igdb.com/v4/games",
bodyContent
);
return new ApiResp
{
Success = httpResponse.IsSuccessStatusCode,
Error = httpResponse.StatusCode == HttpStatusCode.Unauthorized ? ErrorTypes.Unauthorized : null,
Content = await httpResponse.Content.ReadAsStringAsync()
};
}
catch
{
throw;
}
}
}
}
Step 6. Model related to the API response.
namespace Models.Resps
{
public record IGDBGame
{
public string id { get; set; }
public Cover cover { get; set; }
public string name { get; set; }
public string first_release_date { get; set; }
public List<Platform> platforms { get; set; }
}
public record Cover
{
public string id { get; set; }
public string url { get; set; }
public string image_id { get; set; }
}
public record Platform
{
public string id { get; set; }
public string abbreviation { get; set; }
}
}
Step 7. Business class that deserializes the API response.
using ApiRepo;
using Newtonsoft.Json;
namespace Services.Games
{
public static class IGDBGamesApiService
{
public async static Task<List<Models.Resps.IGDBGame>> Get(string search, int startIndex)
{
Models.Resps.ApiResp resp = await IGDBGamesAPIRepo.Get(search, startIndex);
if ((resp is not null) && resp.Success && resp.Content is not null)
{
return JsonConvert.DeserializeObject<List<Models.Resps.IGDBGame>>(resp.Content)
?? throw new Exception("Error getting games");
}
else throw new Exception("Error getting games");
}
}
}
Step 8. In the ViewModel, we will remove the mock list and include a variable to control the search.
![ViewModel]()
Step 9. Let's create the function that will pass the result to the listview.
Step 9.1. This function passes the text entered in the field to the search, receives the result list and processes it for display on the screen.
private async Task LoadIGDBGamesList(int startIndex)
{
List<IGDBGame> resp = await IGDBGamesApiService.Get(SearchText, startIndex);
DateTime? releaseDate = null;
foreach (var item in resp)
{
if (item is null) continue;
releaseDate = item.first_release_date is not null
? DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(item.first_release_date)).UtcDateTime
: null;
ListGames.Add(new UIIGDBGame
{
Id = item.id,
Name = item.name ?? "",
ReleaseDate = releaseDate?.Date.ToString("MM/yyyy") ?? "",
CoverUrl = item.cover?.id is not null
? $"https://images.igdb.com/igdb/image/upload/t_cover_big/{item.cover?.image_id}.jpg"
: "",
Platforms = item.platforms?.Count > 0
? string.Join(", ", item.platforms.Select(p => p.abbreviation))
: ""
});
}
}
Step 9.2. This function sets a time interval of 2.5 seconds to perform the search.
private async Task SearchGamesList()
{
if (Searching || SearchText.Length < 3)
return;
Searching = true;
while (Searching)
{
await Task.Delay(2500);
if (ListGames.Count > 0)
ListGames.Clear();
try
{
await LoadIGDBGamesList(0);
}
catch (Exception)
{
throw;
}
Searching = false;
}
}
Step 9.3. We will add it to SearchText and use it asynchronously.
public string SearchText
{
get => searchText;
set
{
if (searchText != value)
{
SetProperty(ref (searchText), value);
_ = SearchGamesList();
}
}
}
Step 10. We will modify the frontend to include a scroll in the ListView, defining the main layout as a grid, setting the sizes, and defining the ScrollView parameters.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border
Padding="10,0,10,5"
HorizontalOptions="FillAndExpand"
Style="{StaticResource BorderPrimary}">
<components:BorderedEntry
LabelText="Search"
MaxLength="100"
Text="{Binding SearchText}" />
</Border>
<ScrollView
Grid.Row="1"
Orientation="Vertical"
VerticalOptions="FillAndExpand">
<VerticalStackLayout>
<Border
Margin="5"
Padding="5"
BackgroundColor="Transparent"
StrokeShape="RoundRectangle 10"
VerticalOptions="FillAndExpand">
<ListView
CachingStrategy="RecycleElement"
HasUnevenRows="True"
ItemsSource="{Binding ListGames}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:UIIGDBGame">
<ViewCell>
<Border
Margin="0,0,0,5"
Padding="10"
BackgroundColor="#101923"
Stroke="#2B659B"
StrokeShape="RoundRectangle 10">
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Aspect="AspectFit"
HeightRequest="100"
Source="{Binding CoverUrl}"
VerticalOptions="Center"
WidthRequest="150" />
<StackLayout Grid.Column="1" Margin="10,0,0,0">
<Label
FontSize="Large"
LineBreakMode="TailTruncation"
Text="{Binding Name}" />
<Label
FontAttributes="Italic"
FontSize="Micro"
Text="{Binding ReleaseDate, StringFormat='Release: {0:F0}'}"
TextColor="#98BDD3" />
<Label
FontSize="Micro"
Text="{Binding Platforms, StringFormat='Platforms: {0:F0}'}"
TextColor="#98BDD3" />
</StackLayout>
</Grid>
</Border>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Border>
</VerticalStackLayout>
</ScrollView>
</Grid>
Step 11. As a result, we have our screen with the functional search.
![Functional search]()
In the next step, we will add dynamic pagination to the grid and handle loading the information.
Code on git: GamesCatalog git