Previous part: Listing Information from the Local DB in MAUI .NET 9 [GamesCatalog] - Part 15
Step 1. Let’s create a function to remove a game from the list. In GameRepo, we’ll add a function to inactivate the game and declare it in the interface.
public async Task InactivateAsync(int uid, int id, DateTime updatedAt)
{
using var context = DbCtx.CreateDbContext();
await context.Games.Where(x => x.Id == id && x.UserId == uid)
.ExecuteUpdateAsync(y => y
.SetProperty(z => z.UpdatedAt, updatedAt)
.SetProperty(z => z.Inactive, true));
}
Step 1.1. Let’s modify the UpdateStatusAsync so that if the game exists in the local database and is inactive, it is reactivated.
![UpdateStatusAsync]()
Code of UpdateStatusAsync
public async Task UpdateStatusAsync(int id, DateTime updatedAt, GameStatus status, int? rate)
{
using var context = DbCtx.CreateDbContext();
await context.Games.Where(x => x.Id == id).ExecuteUpdateAsync(y => y
.SetProperty(z => z.Inactive, false)
.SetProperty(z => z.UpdatedAt, updatedAt)
.SetProperty(z => z.Status, status)
.SetProperty(z => z.Rate, rate));
}
Step 2. In GameService, we’ll add the InactivateAsync function and declare it in the interface.
public async Task InactivateAsync(int? uid, int id)
{
int _uid = uid ?? 1;
await GameRepo.InactivateAsync(_uid, id, DateTime.Now);
}
Step 3. In GameList.xaml, we’ll set a tap on an item in the list to navigate to the game addition screen.
![GameList.xaml]()
Add the command and use [Ctrl + space] to add the function.
Step 3.1. In GameList.xaml.cs, the function will open the game addition screen, passing the game object from the list.
private void GamesLstVw_ItemTapped(object sender, ItemTappedEventArgs e)
{
if (e.Item is UIGame tappedItem)
Shell.Current.GoToAsync($"{nameof(AddGame)}", true, new Dictionary<string, object>
{
{ "Game", tappedItem }
});
}
Step 4. In AddGameVM.cs, we’ll add a variable to control the visibility of the Expander, which will contain the option to remove a game:
private bool expanderIsVisible = false;
public bool ExpanderIsVisible
{
get => expanderIsVisible;
set => SetProperty(ref expanderIsVisible, value);
}
Step 4.1. In the BuildGameStatus function, we’ll set ExpanderIsVisible to true when the game exists in the local database and is not inactive.
![BuildGameStatus function]()
Code of BuildGameStatus
public async Task BuildGameStatus()
{
var gameDTO = await gameService.GetByIGDBIdAsync(int.Parse(IgdbId));
if (gameDTO is null) return;
Id = gameDTO.Id;
if (!gameDTO.Inactive)
{
if (Id is not null)
ExpanderIsVisible = true;
switch (gameDTO.Status)
{
case GameStatus.Want: _ = Want(); break;
case GameStatus.Playing: _ = Playing(); break;
case GameStatus.Played:
_ = Played();
Rate = gameDTO.Rate;
break;
}
}
}
Step 4.2. And we’ll create a command to inactivate the game, removing it from our list:
[RelayCommand]
public async Task Inactivate()
{
if (await Application.Current.Windows[0].Page.DisplayAlert("Confirm", "Remove from list?", "Yes", "Cancel"))
{
_ = gameService.InactivateAsync(null, Id.Value);
if (DeviceInfo.Platform == DevicePlatform.iOS || DeviceInfo.Platform == DevicePlatform.Android)
{
ToastDuration duration = ToastDuration.Short;
var toast = Toast.Make("Game removed", duration, 15);
await toast.Show();
}
else
await Application.Current.Windows[0].Page.DisplayAlert("Success", "Game removed", null, "Ok");
await Shell.Current.GoToAsync("..");
}
}
Step 5. In AddGame.xaml, let’s add the toolkit usage:
![AddGame.xaml]()
Code
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
Step 5.1. And we’ll create an Expander, which will currently only contain the option to remove the game from the list:
![Expander]()
Code for the Expander
<toolkit:Expander
Grid.Row="1"
Margin="0,0,10,0"
BackgroundColor="Transparent"
Direction="Down"
HorizontalOptions="End"
IsVisible="{Binding ExpanderIsVisible}"
VerticalOptions="Start"
ZIndex="2">
<toolkit:Expander.Header>
<Button
BackgroundColor="{StaticResource PrimaryElementColor}"
ContentLayout="Top,0"
CornerRadius="25"
HeightRequest="50"
HorizontalOptions="End"
Opacity="0.9"
VerticalOptions="End"
WidthRequest="50">
<Button.ImageSource>
<FontImageSource
FontFamily="FontAwesomeIcons"
Glyph="{x:Static Icons:IconFont.EllipsisVertical}"
Size="30"
Color="Black" />
</Button.ImageSource>
</Button>
</toolkit:Expander.Header>
<toolkit:Expander.Content>
<VerticalStackLayout Margin="0,10,0,0" Spacing="10">
<Button
BackgroundColor="{StaticResource PrimaryElementColor}"
Command="{Binding InactivateCommand}"
Style="{StaticResource ExpanderButton}">
<Button.ImageSource>
<FontImageSource
FontFamily="FontAwesomeIcons"
Glyph="{x:Static Icons:IconFont.Trash}"
Size="25"
Color="Black" />
</Button.ImageSource>
</Button>
</VerticalStackLayout>
</toolkit:Expander.Content>
</toolkit:Expander>
Step 6. Let’s add styles for the buttons inside the Expander:
![Add styles for the buttons]()
Code
<ContentPage.Resources>
<Style x:Key="ExpanderButton" TargetType="Button">
<Setter Property="ContentLayout" Value="Top,0" />
<Setter Property="CornerRadius" Value="25" />
<Setter Property="HeightRequest" Value="50" />
<Setter Property="WidthRequest" Value="50" />
<Setter Property="HorizontalOptions" Value="End" />
<Setter Property="Opacity" Value="0.9" />
<Setter Property="VerticalOptions" Value="Start" />
</Style>
</ContentPage.Resources>
Step 7. In GameListVM.cs, we’ll clear the list when the screen is opened.
![GameListVM.cs]()
Code for ApplyQueryAttributes
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query != null && query.TryGetValue("GameStatus", out object? outValue))
{
if (outValue is null) throw new ArgumentNullException("gameStatus");
GameStatus = (GameStatus)Convert.ToInt32(outValue);
TitleStatus = GameStatus switch
{
GameStatus.Want => "Want to play",
GameStatus.Playing => "Playing",
GameStatus.Played => "Played",
_ => throw new ArgumentOutOfRangeException("gameStatus"),
};
}
else throw new ArgumentNullException(nameof(query));
CurrentPage = 1;
if (Games.Count > 0)
Games.Clear();
_ = LoadGames();
}
Step 8. With this, we have the option to remove a game from the list.
![Option of removing a game]()
Initial state of the Expander.
![Initial state of the Expander]()
On tap, it reveals the removal option. If the game is removed, we return to the updated game list.
Step 9. Since we return to the game list, let’s add a search function to this page. For this, we’ll add a variable in GameListVM.cs that will be bound to the search field:
string searchText = "";
public string SearchText
{
get => searchText;
set
{
if (searchText != value)
{
SetProperty(ref (searchText), value);
_ = SearchGamesList();
}
}
}
Step 10. Then, let’s add a function that performs the search if the search text hasn’t changed for a second and a half:
private CancellationTokenSource? searchDelayTokenSource;
private async Task SearchGamesList()
{
if (SearchText.Length < 3)
return;
searchDelayTokenSource?.Cancel();
searchDelayTokenSource = new CancellationTokenSource();
var token = searchDelayTokenSource.Token;
await Task.Delay(1500, token);
if (!token.IsCancellationRequested)
{
if (Games.Count > 0)
Games.Clear();
CurrentPage = 1;
await LoadGames();
}
}
If any text is entered within that second and a half window, the task is canceled before the search begins.
Step 10.1. We’ll pass the search text to the service:
![Service]()
Code for LoadGames() and declaration of searchSemaphore:
private readonly SemaphoreSlim searchSemaphore = new(1, 1);
private async Task LoadGames()
{
IsBusy = true;
await searchSemaphore.WaitAsync();
try
{
string _searchText = "";
if (!string.IsNullOrEmpty(SearchText))
_searchText = SearchText.ToLower();
List<GameDTO> games = await gameService.GetByStatusAsync(null, GameStatus, CurrentPage, _searchText);
if (games.Count < 10) CurrentPage = -1;
foreach (var game in games)
{
Games.Add(new UIGame
{
Id = game.IGDBId.ToString() ?? throw new ArgumentNullException("IGDBId"),
LocalId = game.Id,
CoverUrl = game.CoverUrl ?? "",
Status = game.Status,
Rate = game.Status == GameStatus.Played ? game.Rate : 0,
Name = game.Name,
ReleaseDate = game.ReleaseDate ?? "",
Platforms = game.Platforms ?? "",
Summary = game.Summary ?? "",
});
}
}
finally
{
searchSemaphore.Release();
}
IsBusy = false;
}
Step 10.2. In the service, we’ll modify the GetByStatusAsync function to conditionally call the function that performs a text-based search in the local database.
![GetByStatusAsync function]()
Code for GetByStatusAsync
public async Task<List<GameDTO>> GetByStatusAsync(int? uid, GameStatus gameStatus, int page, string searchText)
{
int _uid = uid ?? 1;
if (string.IsNullOrEmpty(searchText))
return await GameRepo.GetByStatusAsync(_uid, gameStatus, page);
else
return await GameRepo.GetByStatusAsync(_uid, gameStatus, page, searchText);
}
![Reference]()
Update the interface with the new field.
Step 10.3. In GameRepo.cs, let’s add the function that performs the text search and declare it in the interface:
public async Task<List<GameDTO>> GetByStatusAsync(int uid, GameStatus gameStatus, int page, string searchText)
{
using var context = DbCtx.CreateDbContext();
return await context.Games
.Where(x => x.UserId.Equals(uid) && x.Status == gameStatus && x.Inactive == false && EF.Functions.Like(x.Name, $"%{searchText}%"))
.OrderByDescending(x => x.UpdatedAt).Skip((page - 1) * pageSize).Take(pageSize)
.ToListAsync();
}
Step 11. Now our search works on the screen:
![Search works on screen]()
![Goddwar]()
Selecting a game from the list takes us to the game details screen. There, we can select and confirm the option to remove the game from the list.
![Playing]()
Removing the searched game from the list returns us to the screen with the game no longer appearing in the results.
Next part: Setting the App Icon and SplashScreen in MAUI [GamesCatalog] 17
Code on git: GamesCatalog