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