.NET MAUI  

Listing Information from the Local DB in MAUI .NET 9 [GamesCatalog] - Part 15

Previous part: Horizontal List of Overlapping Images in MAUI [GamesCatalog] - Part 14

Step 1. Let's create the page responsible for the local listing.

Local listing

Step 1.1. Create the ContentPage GameList.xaml, the class GameListVM.cs, and link both in GameList.xaml.cs.

Code GameListVM.cs

    public partial class GameListVM : ViewModelBase
    {
    }

Code GameList.xaml.cs

public partial class GameList : ContentPage
{
	public GameList(GameListVM gameListVM)
	{
		InitializeComponent();

        BindingContext = gameListVM;
    }
}

Step 1.2. Add the route.

Add Route

Code MauiProgram.cs

services.AddTransientWithShellRoute<GameList, GameListVM>(nameof(GameList));

Step 2. In MainVM.cs, we'll create the routes for listing local games.

    [RelayCommand]
    private Task ListWant() => GoToAsync($"{nameof(GameList)}?GameStatus={(int)GameStatus.Want}");

    [RelayCommand]
    private Task ListPlaying() => GoToAsync($"{nameof(GameList)}?GameStatus={(int)GameStatus.Playing}");

    [RelayCommand]
    private Task ListPlayed() => GoToAsync($"{nameof(GameList)}?GameStatus={(int)GameStatus.Played}");

    private static Task GoToAsync(string route)
    {
        return Shell.Current.GoToAsync(route, true);
    }

Step 3. In Main.xaml, in the border that represents the card of games with the status "want to play", we'll add the command and a function for the tap gesture.

Main.xaml

Step 3.1. In the function created in Main.xaml.cs, we’ll add an animation to represent the tap on the card.

    private void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e)
    {
        var cell = sender as Border;
        cell.Opacity = 0.5;
        _ = cell.FadeTo(1, 1000);
    } 

Step 3.2. Repeat with the commands for "playing" and "played":

Played

Code PlayingCard

    <Border.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding PlayingListCommand}" />
        <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
    </Border.GestureRecognizers>

Code PlayedCard

       <Border.GestureRecognizers>
           <TapGestureRecognizer Command="{Binding PlayedListCommand}" />
           <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
       </Border.GestureRecognizers>

Step 3.3. This is how we call the GameList.xaml page.

Step 4. In GameRepo.cs, implement the function that retrieves games by status from the local database and declares them in the interface.

GameRepo.cs

Code

       public async Task<List<GameDTO>> GetByStatusAsync(int uid, GameStatus gameStatus, int page)
       {
           using var context = DbCtx.CreateDbContext();
           return await context.Games
               .Where(x => x.UserId.Equals(uid) && x.Status == gameStatus && x.Inactive == false)
               .OrderByDescending(x => x.UpdatedAt).Skip((page - 1) * pageSize).Take(pageSize)
               .ToListAsync();
       }

Step 5. In GameService.cs, implement the function that uses GetByStatusAsync and declare it in the interface.

   public async Task<List<GameDTO>> GetByStatusAsync(int? uid, GameStatus gameStatus, int page)
   {
       int _uid = uid ?? 1;

       return await GameRepo.GetByStatusAsync(_uid, gameStatus, page);
   }

Step 6. Let's create a model to handle the local game listing, which inherits the fields from UIIGDBGame.

namespace GamesCatalog.Models
{
    public class UIGame : UIIGDBGame
    {
        public int LocalId { get; set; }

        public GameStatus Status { get; set; }

        public int? Rate { get; set; }

        public bool RateIsVisible => Rate > 0;
    }
}

Step 7. In GameListVM.cs, it will be very similar to the API-based game listing.

GameListVM.cs

It uses the IGameService and reads parameters passed via query.

    public partial class GameListVM(IGameService gameService) : ViewModelBase, IQueryAttributable

It has a list that is passed to the front end.

      private ObservableCollection<UIGame> games = [];

      public ObservableCollection<UIGame> Games
      {
          get => games;
          set => SetProperty(ref games, value);
      }

Code

Reads the status passed by the query, sets the page title, and loads the first page of the list.

        private GameStatus GameStatus { get; set; }

        private string titleStatus = "";

        public string TitleStatus
        {
            get => titleStatus;
            set => SetProperty(ref titleStatus, value);
        }

        private int CurrentPage { get; set; }

        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;

            _ = LoadGames();
        }

Query

With the results from GetByStatusAsync(), it creates the listing for the front end.

        private async Task LoadGames()
        {
            IsBusy = true;

            List<GameDTO> games = await gameService.GetByStatusAsync(null, GameStatus, CurrentPage);

            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 : null,
                    Name = game.Name,
                    ReleaseDate = game.ReleaseDate ?? "",
                    Platforms = game.Platforms ?? "",
                    Summary = game.Summary ?? "",
                });
            }

            IsBusy = false;
        }

Loads the list incrementally, 10 at a time.

        [RelayCommand]
        public async Task LoadMore()
        {
            if (CurrentPage < 0) return;

            CurrentPage += 1;
            await LoadGames();
        }

Step 8. In GameList.xaml, we’ll place the list inside a general grid.

General grid

Code

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="GamesCatalog.Views.Game.GameList"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:behaviors="clr-namespace:GamesCatalog.Utils.Behaviors"
    xmlns:model="clr-namespace:GamesCatalog.Models"
    xmlns:vm="clr-namespace:GamesCatalog.ViewModels.Game"
    Title="{Binding TitleStatus}"
    x:DataType="vm:GameListVM">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Border
            Margin="5"
            Padding="5"
            StrokeShape="RoundRectangle 10">
            <ListView x:Name="GamesLstVw"
                CachingStrategy="RecycleElement"
                HasUnevenRows="True"
                ItemsSource="{Binding Games}"
                SelectionMode="None">
                <ListView.Behaviors>
                    <behaviors:InfiniteScrollBehavior LoadMoreCommand="{Binding LoadMoreCommand}" />
                </ListView.Behaviors>
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="model:UIGame">
                        <ViewCell>
                            <Border
                                Margin="0,0,0,5"
                                Padding="10"
                                BackgroundColor="#101923"
                                Stroke="#2B659B"
                                StrokeShape="RoundRectangle 10">
                                <Grid Padding="10">
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="Auto" />
                                        <RowDefinition Height="*" />
                                    </Grid.RowDefinitions>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>
                                    <Label
                                        Grid.ColumnSpan="2"
                                        Margin="0,0,0,10"
                                        FontSize="20"
                                        LineBreakMode="CharacterWrap"
                                        Text="{Binding Name}"
                                        TextColor="White" />
                                    <Image
                                        Grid.Row="1"
                                        Grid.Column="0"
                                        Aspect="AspectFit"
                                        HeightRequest="220"
                                        Source="{Binding CoverUrl}"
                                        VerticalOptions="Center"
                                        WidthRequest="170" />
                                    <StackLayout
                                        Grid.Row="1"
                                        Grid.Column="1"
                                        Margin="10,0,0,0">
                                        <Label
                                            FontAttributes="Italic"
                                            FontSize="15"
                                            Text="{Binding ReleaseDate, StringFormat='Release: {0:F0}'}"
                                            TextColor="#98BDD3" />
                                        <Label
                                            FontSize="15"
                                            Text="{Binding Platforms, StringFormat='Platforms: {0:F0}'}"
                                            TextColor="#98BDD3" />
                                    </StackLayout>
                                </Grid>
                            </Border>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
                <ListView.Footer>
                    <ActivityIndicator
                        HorizontalOptions="Center"
                        IsRunning="{Binding IsBusy}"
                        IsVisible="{Binding IsBusy}"
                        VerticalOptions="Center"
                        Color="{StaticResource ActiveColor}" />
                </ListView.Footer>
            </ListView>
        </Border>
    </Grid>
</ContentPage>

Step 9. The list is now being displayed on the screen.

Being displayed

Step 10. Let's add a property to the rating bar to control the icon sizes.

Icon sizes

Code

    public static readonly BindableProperty IconFontSizeProperty = BindableProperty.Create(
    propertyName: nameof(IconFontSize), returnType: typeof(int), declaringType: typeof(BorderedEntry), defaultValue: 20, defaultBindingMode: BindingMode.TwoWay);

    public int IconFontSize { get => (int)GetValue(IconFontSizeProperty); set { SetValue(IconFontSizeProperty, value); } }

Step 11. Add the property to the icons on the screen, on all 10.

Screen

Code

           FontSize="{Binding Source={x:Reference this}, Path=IconFontSize}"

Step 12. Add the rating bar to the listing.

Rating Bar

Code

                                        <components:RatingBar
                                            HorizontalOptions="Start"
                                            IconFontSize="10"
                                            IsEnabled="False"
                                            IsVisible="{Binding RateIsVisible}"
                                            Rate="{Binding Rate}" />

Step 13. We now have our rating bar displayed on the screen for the "played" status.

Status

In the next step, we’ll create a search function for this listing, enable game editing, and add an option to remove the game from the list.

Code on git: GamesCatalog git