Search code examples
iosmauiobservablecollectionmac-catalystgrouped-list

Grouped Collection View shows empty groups on iOS


I have a MAUI application with a view binded to a viewmodel containing a grouped CollectionView with these properties set:

        <CollectionView
           x:Name="MyList"
           VerticalOptions="FillAndExpand"
           ItemsSource="{Binding CollectionViewItemSourceGrouped}"
           SelectionMode="Single"                   
           SelectedItem="{Binding MyItem, Mode=TwoWay}"
           SelectionChangedCommand="{Binding MySelectionCommand}"
           RemainingItemsThreshold="2"
           RemainingItemsThresholdReachedCommand="{Binding LoadMoreItemsCommand}"
           IsGrouped="True"
           EmptyView="No item found.">

I get and group the source items from an awaitable Web api and I correctly get the collection grouped when I first navigate into the view and if I start toggling back and forth the switch filtering the list. Unfortunately, when I navigate into a detail page and back to the collection through Shell navigation, if I act on the switch again and I reload the list I always get a number of empty groups and it graphically results into a big mess. The object used as a source for the collection is CollectionViewItemSourceGrouped, it is an ObservableCollection<ItemsGroup> which has this implementation:

public class ItemsGroup : ObservableRangeCollection<ItemModel>
{
    public DateTime Date { get; private set; }

    public ItemsGroup(DateTime date, ObservableRangeCollection<ItemModel> list) : base(list)
    {
        Date = date;
    }
}

Here's also how the data are loaded:

        try
        {
            if (InLoading)
                return;

            InLoading = true;

            //Selectio item gets blanked
            if (MyItem is not null)
                MyItem = null;

            CollectionViewItemSourceGrouped = new();

            paginationIndex = 1;

            IEnumerable<ItemsGroup> grpItems = await LoadItemsSub(0, MaxItemsPerPage);

            if (grpItems != null && grpItems.Any())
                CollectionViewItemSourceGrouped = new ObservableRangeCollection<ItemsGroup>(grpItems.OrderByDescending(s => s.Date));
            else           
                CollectionViewItemSourceGrouped = new();
        }
        catch (LocalException ex)
        {
            UIMessagesManager.ShowMessage(Stringhe.DANGER, ex.Message);
        }
        catch (Exception ex)
        {
            AppCenterEventManager.TrackError(ex, string.Empty);

            UIMessagesManager.ShowMessage(Stringhe.DANGER, Stringhe.UNKNOWN_ERROR);
        }
        finally
        {
            InLoading = false;
        }

Every time I reload data, I make sure the object CollectionViewItemSourceGrouped is set to null or is a new empty object. I would expect the collection to be empty, rather this keeps on showing empty groups with only the header set. How can I get rid of them and let the collection properly refresh?

This only happens on iOS and MacCatalyst. It nicely works on Android and WinUI.

Here's a pic of what I get once I reload data coming back from a detail page.

In the image above, the header 01/05/2023 is unexpected and appears as an empty group. But the item source has already been reassigned properly in the view model. Here's what I expect:

Expected behavior in reloading the list

I tried literally everything but with no luck on iOS and MacCatalyst.

UPDATE 1

Here's a GitHub public repository with a sample project. Try to execute it on iOS Simulator and you will be able to see this strange behavior when VerticalOptions=FillAndExpand for the CollectionView. [See picture]

Wrong rendering for groups on iOS

GitHub Repository - GroupedCollectionSample

UPDATE 2

Notice that if I set VerticalOptions for the container (StackLayout) I keep on getting the same "empty" group headers odd behavior. I currently "solved" leaving the StackLayout and setting a fixed HeightRequest to the CollectionView, but of course it doesn't fit to all devices.

I also tried to set it into a Grid as suggested and here's the code. This doesn't fix anything because the collection keeps on expanding out of the screen and so the scroll to the bottom becomes impossible (I think due to the fact that the container doesn't have a fixed end). I dunno what to do since: changing the container VerticalOptions causes the issue, leaving it at default causes the CollectionView not to scroll to the bottom.

<StackLayout>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Heigth="40" />
            <RowDefinition Heigth="*" />  
        </Grid.RowDefinitions>
        
        <StackLayout
            Grid.Row = "0"
            HeightRequest="40"
            Orientation="Horizontal">
            
            <Switch
                VerticalOptions="Center"
                Margin="10,0"
                IsToggled="{Binding FilterData}">
                <Switch.Behaviors>
                    <toolkit:EventToCommandBehavior
                        EventName="Toggled"
                        Command="{Binding FilterDataCommand}" />
                </Switch.Behaviors>
            </Switch>

            <Label 
                Text="Only enabled users"/>
        </StackLayout>

        <CollectionView
            x:Name="ListUsers"
            Grid.Row = "1"
            VerticalOptions="FillAndExpand"
            ItemsSource="{Binding ListUsersGroup}"
            SelectionMode="Single"                   
            SelectedItem="{Binding CurrentUser, Mode=TwoWay}"
            SelectionChangedCommand="{Binding OpenDetailCommand}"
            IsGrouped="True"
            EmptyView="No user found.">

            <CollectionView.GroupHeaderTemplate>
                <DataTemplate x:DataType="extModels:UsersGroup">
                    <Label
                        Padding="10,8"
                        BackgroundColor="Gray"
                        TextColor="Black"
                        Text="{Binding Date}"
                        FontAttributes="Bold"
                        FontSize="15"/>
                </DataTemplate>
            </CollectionView.GroupHeaderTemplate>

            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="extModels:User">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="auto" />
                            <ColumnDefinition Width="auto" />
                        </Grid.ColumnDefinitions>

                        <Grid.RowDefinitions>
                            <RowDefinition Height="50"/>
                        </Grid.RowDefinitions>
                        <Label
                            Grid.Column="0"
                            Margin="20,0"
                            Text="{Binding FirstName}">
                        </Label>

                        <Label 
                            Grid.Column="1"
                            Text="{Binding LastName}" >
                        </Label>
                    </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
</StackLayout>

Solution

  • I did not have time to review everything, but this here:

    <StackLayout>
            <StackLayout
                HeightRequest="40"
                Orientation="Horizontal">
                
                <Switch
                    VerticalOptions="Center"
                    Margin="10,0"
                    IsToggled="{Binding FilterData}">
                    <Switch.Behaviors>
                        <toolkit:EventToCommandBehavior
                            EventName="Toggled"
                            Command="{Binding FilterDataCommand}" />
                    </Switch.Behaviors>
                </Switch>
    
                <Label 
                    Text="Only enabled users"/>
            </StackLayout>
    
            <CollectionView
                x:Name="ListUsers"
                VerticalOptions="FillAndExpand"
                ItemsSource="{Binding ListUsersGroup}"
                SelectionMode="Single"                   
                SelectedItem="{Binding CurrentUser, Mode=TwoWay}"
                SelectionChangedCommand="{Binding OpenDetailCommand}"
                IsGrouped="True"
                EmptyView="No user found.">
    

    This is more than enough... What are you "filling"? How are you filling something, that has no bottom?

    Also, you see now, how "more code provided" makes much more sense. In your original question, because I did not see the parent, I could not notice this.

    Edit: Limiting the height of the item in the container, is one way. Now, usually you will want to limit the container itself.

    In your scenario, A Grid, with RowDefinitions= "40,*" will give you the desired result. (first row 40, second row, as much as it takes). This way, you can place your StackLayout in Row = 0 , and CollectionView in Row = 1, and let them take all the space.

    (So this way your design will be working on different phone screens.)

    EDIT2:

    <StackLayout> <<< This here has no height restriction. Delete this.
    <Grid> <<< This will match your view. Keep it.
        <Grid.RowDefinitions>
            <RowDefinition Heigth="40" />
            <RowDefinition Heigth="*" />  <<< this here reads "fill"
        </Grid.RowDefinitions>
        
        <StackLayout
            Grid.Row = "0"
            HeightRequest="40"
            Orientation="Horizontal">
            
            <Switch
                VerticalOptions="Center"
                Margin="10,0"
                IsToggled="{Binding FilterData}">
                <Switch.Behaviors>
                    <toolkit:EventToCommandBehavior
                        EventName="Toggled"
                        Command="{Binding FilterDataCommand}" />
                </Switch.Behaviors>
            </Switch>
    
            <Label 
                Text="Only enabled users"/>
        </StackLayout>
    
        <CollectionView