.NET MAUI  

Asynchronously Grouping Totals in MAUI [GamesCatalog] - Part 13

Previous part: Dynamic Styles and Appearance Behavior in MAUI [GamesCatalog] - Part 12

Step 1. In MainVM.cs, let’s add 3 variables for binding the quantities on the main page.

private int qtdWant = 0;
private int qtdPlaying = 0;
private int qtdPlayed = 0;

public int QtdWant
{
    get => qtdWant;
    set => SetProperty(ref qtdWant, value);
}

public int QtdPlaying
{
    get => qtdPlaying;
    set => SetProperty(ref qtdPlaying, value);
}

public int QtdPlayed
{
    get => qtdPlayed;
    set => SetProperty(ref qtdPlayed, value);
}

Step 2. In the frontend, we’ll create a VerticalStackLayout that will contain the components related to the game status lists.

This component will be a Grid with a border, and inside it, we’ll have a body and a footer.

In the footer, we’ll place 3 components: an icon for the status, the quantity, and an arrow to indicate the card is clickable.

 VerticalStackLayout

Code

<VerticalStackLayout Margin="5" VerticalOptions="Fill">
    <Border Margin="0,5,0,5" Style="{StaticResource BorderCardItem}">
        <Grid
            RowDefinitions="*,*"
            RowSpacing="5"
            Style="{StaticResource GridCardItem}">
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <Label
                Grid.Row="1"
                Margin="0"
                FontSize="40"
                HorizontalOptions="Start"
                Style="{StaticResource LblImgIcon}"
                Text="{x:Static Icons:IconFont.Clock}"
                VerticalOptions="End" />

            <Label
                Grid.Row="1"
                FontSize="20"
                HorizontalOptions="Center"
                Style="{StaticResource LblText}"
                Text="{Binding QtdWant, StringFormat='{0} Want to Play'}"
                VerticalOptions="Center" />

            <Label
                Grid.Row="1"
                Style="{StaticResource ImgAngleRight}" />
                
        </Grid>
    </Border>                   
</VerticalStackLayout>

Step 3. Let’s add styles related to these components.

Component

Code

<Style x:Key="BorderCardItem" TargetType="Border">
    <Setter Property="BackgroundColor" Value="{StaticResource SecondaryBGColor}" />
    <Setter Property="Stroke" Value="{StaticResource ActiveColor}" />
    <Setter Property="StrokeShape" Value="RoundRectangle 10" />
    <Setter Property="Padding" Value="10" />
</Style>

<Style x:Key="GridCardItem" TargetType="Grid">
    <Setter Property="VerticalOptions" Value="Start" />
    <Setter Property="HorizontalOptions" Value="Fill" />
</Style>

<Style x:Key="LblImgIcon" TargetType="Label">
    <Setter Property="FontFamily" Value="FontAwesomeIcons" />
    <Setter Property="TextColor" Value="{StaticResource White}" />
</Style>

<Style x:Key="ImgAngleRight" TargetType="Label">
    <Setter Property="FontFamily" Value="FontAwesomeIcons" />
    <Setter Property="VerticalTextAlignment" Value="Center" />
    <Setter Property="FontSize" Value="40" />
    <Setter Property="TextColor" Value="{StaticResource White}" />
    <Setter Property="HorizontalOptions" Value="End" />
    <Setter Property="Text" Value="{x:Static Icons:IconFont.AngleRight}" />
</Style>

<Style x:Key="LblQtd" TargetType="Label">
    <Setter Property="TextColor" Value="{StaticResource White}" />
    <Setter Property="VerticalOptions" Value="End" />
    <Setter Property="VerticalTextAlignment" Value="End" />
    <Setter Property="FontSize" Value="14" />
    <Setter Property="FontAttributes" Value="Bold" />
    <Setter Property="Margin" Value="5,0,0,0" />
</Style>

Step 4. We’ll duplicate the Border that represents the card two more times and change the icon, variable, and text according to each game status.

Border

Code

<Border Margin="0,5,0,5" Style="{StaticResource BorderCardItem}">
    <Grid RowDefinitions="*,*" RowSpacing="5" Style="{StaticResource GridCardItem}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Label 
            Grid.Row="1" 
            Margin="0,0,0,0" 
            FontSize="40" 
            HorizontalOptions="Start" 
            Style="{StaticResource LblImgIcon}" 
            Text="{x:Static Icons:IconFont.Gamepad}" 
            VerticalOptions="End" />

        <Label 
            Grid.Row="1" 
            FontSize="20" 
            HorizontalOptions="Center" 
            Style="{StaticResource LblQtd}" 
            Text="{Binding QtdPlaying, StringFormat='{0} Playing'}" 
            VerticalOptions="Center" />

        <Label Grid.Row="1" Style="{StaticResource ImgAngleRight}" />
    </Grid>
</Border>

<Border Margin="0,5,0,5" Style="{StaticResource BorderCardItem}">
    <Grid RowDefinitions="*,*" RowSpacing="5" Style="{StaticResource GridCardItem}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Label 
            Grid.Row="1" 
            Margin="0,0,0,0" 
            FontSize="40" 
            HorizontalOptions="Start" 
            Style="{StaticResource LblImgIcon}" 
            Text="{x:Static Icons:IconFont.Check}" 
            VerticalOptions="End" />

        <Label 
            Grid.Row="1" 
            FontSize="20" 
            HorizontalOptions="Center" 
            Style="{StaticResource LblQtd}" 
            Text="{Binding QtdPlayed, StringFormat='{0} Played'}" 
            VerticalOptions="Center" />

        <Label Grid.Row="1" Style="{StaticResource ImgAngleRight}" />
    </Grid>
</Border>

Step 5. Now we have our 3 cards representing the game statuses on the screen.

 Game statuses

Step 6. Let’s populate the totals. For that, we’ll create a model to handle the totals inside the Models project.

using Models.DTOs;
namespace Models
{
    public record TotalGroupedByStatus
    {
        public int Total { get; set; }
        public GameStatus? Status { get; set; }
        public string?[] LastFiveIGDBIdsByUpdatedAt { get; set; } = Array.Empty<string>();
    }
}

Step 7. In the Repo project, inside the GameRepo.cs class, we add the function to the class and declare it in the interface.

public List<TotalGroupedByStatus>? GetTotalsGroupedByStatusAsync(int uid)
{
    using var context = DbCtx.CreateDbContext();
    var result = context.Games
        .Where(x => x.UserId.Equals(uid) && x.Inactive == false)
        .GroupBy(x => x.Status)
        .Select(x => new TotalGroupedByStatus
        {
            Status = x.Key,
            Total = x.Count(),
            LastFiveIGDBIdsByUpdatedAt = x.OrderByDescending(g => g.UpdatedAt)
                .Take(5)
                .Select(g => g.IGDBId.ToString())
                .ToArray()
        })
        .AsEnumerable();
    return result.ToList();
}

This way, we group the totals according to their statuses.

Step 7.1. Declare the function in the interface.

Step 8. In GameService.cs, create the function and declare it in the interface.

public List<TotalGroupedByStatus>? GetTotalsGroupedByStatus(int? uid = null)
{
    int _uid = uid ?? 1;
    return GameRepo.GetTotalsGroupedByStatusAsync(_uid);
}

Step 9. In MainVM.cs, let’s create a function that will be triggered every time the page comes into focus.

MainVM

Function code

private readonly SemaphoreSlim SetTotalsGroupedByStatusSemaphore = new(1, 1);

public async Task SetTotalsGroupedByStatus()
{
    await SetTotalsGroupedByStatusSemaphore.WaitAsync();

    try
    {
        var totalsGroupedByStatus = gameService.GetTotalsGroupedByStatus();

        if (totalsGroupedByStatus is not null && totalsGroupedByStatus.Count > 0)
        {
            QtdWant = totalsGroupedByStatus.FirstOrDefault(x => x.Status == GameStatus.Want)?.Total ?? 0;
            QtdPlaying = totalsGroupedByStatus.FirstOrDefault(x => x.Status == GameStatus.Playing)?.Total ?? 0;
            QtdPlayed = totalsGroupedByStatus.FirstOrDefault(x => x.Status == GameStatus.Played)?.Total ?? 0;
        }
        else
        {
            QtdPlayed = QtdPlaying = QtdWant = 0;
        }
    }
    finally
    {
        SetTotalsGroupedByStatusSemaphore.Release();
    }
}

Code to call the function on Appearing.

_ = SetTotalsGroupedByStatus();

Step 10. Now, we have our totals displayed on the main screen.

Main Screen

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