.NET MAUI  

Search and Removal of Item from Local List in MAUI [GamesCatalog] 16

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