Search code examples
c#data-bindingmauiobservablecollectioncollectionview

CollectionView jumps back to top when populating with ObservableCollection in .NET MAUI


Why does my <CollectionView/> keep jumping back to top when I populate it using an ObservableCollection?

Solution found! No clue how that works, but replacing the outer <VerticalStackLayout> by a <Grid> solved the issue. Must be some sort of scroll behavior interference...

The observable collection is bound to the collection view and will display data. It will load more in scroll, but after loading more it jumps back to the top of the page. It worked in my previous projects while getting data directly from an API source. This time I'm trying to get data from a SQLite database. This should work, right? Or have I found a bug?

I suspected ItemsUpdatingScrollMode="KeepScrollOffset" to be the culprit but no matter how I set it, even without it, my CollectionView keeps jumping back to top. Most of the time not even completely, just enough for the third item to display halfway. It also populates twice. While I have set the items limit currently to 7, once I reach the threshold limit, the ObservableCollection is populated with 7 more items and then another 7 items.

View

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:m="clr-namespace:NewProject.Models"
             xmlns:vm="clr-namespace:NewProject.ViewModels"
             xmlns:toolkit="clr-namespace:CommunityToolkit.Maui;assembly=CommunityToolkit.Maui"
             x:Class="NewProject.Views.ItemsPage"
             x:DataType="vm:ItemsViewModel"
             Title="{Binding Title}">
    <!-- Replaced VerticalStackLayout with Grid to solve the issue! -->
    <VerticalStackLayout>
        <Image Style="{StaticResource Header_Image}"/>
        <CollectionView ItemsSource="{Binding Items, Mode=OneTime}"
                        RemainingItemsThresholdReachedCommand="{Binding PopulateCommand}"
                        ItemsUpdatingScrollMode="KeepScrollOffset"
                        RemainingItemsThreshold="1">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="m:Item">
                    <Frame Style="{StaticResource ItemList_Item}">
                        <Grid ColumnDefinitions="5*,*,*">
                            <Label Style="{StaticResource ItemList_ItemName}"
                                   Text="{Binding Name, Mode=OneTime}"
                                   Grid.Column="0" Grid.Row="0"/>
                        </Grid>
                    </Frame>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </VerticalStackLayout>
</ContentPage>

ViewModel

public class ItemsViewModel : BaseViewModel
{
    private IItemRepository repository;
    private int limit = 7;

    public Command PopulateCommand { get; }

    public ObservableCollection<Item> Items { get; } = new();

    public ItemsViewModel(IItemRepository repository)
    {
        this.repository = repository;

        PopulateCommand = new Command(Populate);

        Populate();
    }

    public void Populate()
    {
        if (repository is not null)
        {
            this.Loading = true;

            if (repository.ReadAll().Count > Items.Count)
            {
                var items= repository.ReadAll();

                foreach (var item in items.Skip(Items.Count).Take(limit))
                {
                    Items.Add(item);
                }
            }

            this.Loading = false;
        }
    }
}

Repository interface

public interface IItemRepository
{
    void Create(Item item);
    Item Read(int ItemId);
    void Update(Item item);
    void Delete(int ItemId);
    List<Item> ReadAll();
}

Repository

public class ItemRepository : IItemRepository
{
    private readonly SQLiteConnection conn;

    public ItemRepository()
    {
        if (conn is not null)
        {
            return;
        }

        var path = Path.Combine(FileSystem.AppDataDirectory, "item.db");
        conn = new SQLiteConnection(path);

        conn.CreateTable<Item>();
    }

    public void Create(Item item)
    {
        conn.Insert(item);
    }

    public Item Read(int ItemId)
    {
        return conn.Find<Item>(ItemId);
    }

    public void Update(Item item)
    {
        conn.Update(item);
    }

    public void Delete(int ItemId)
    {
        conn.Delete(ItemId);
    }

    public List<Item> ReadAll()
    {
        return conn.Table<Item>().OrderBy(item => item.Name).ToList();
    }
}

Solution

  • Since you are using CollectionView, you can integrate the header image withing the CollectionView.Header which can present a header that scroll with the items in the list.

    You can refer to the sample code below:

    <ContentPage  ...>
            <CollectionView ItemsSource="{Binding Monkeys}">
    
                <CollectionView.Header>
                    <StackLayout>
                        <Image Style="{StaticResource Header_Image}"/>                        
                    </StackLayout>
                </CollectionView.Header>
    
                ...Omitted for brevity
    
    

    Or you can use <Grid> instead of <VerticalStackLayout> which figure out by yourself to fix the issue like below:

    <Grid>
          <Image Style="{StaticResource Header_Image}"/>
          <CollectionView ItemsSource="{Binding Items, Mode=OneTime}"
                            RemainingItemsThresholdReachedCommand="{Binding PopulateCommand}"
                            ItemsUpdatingScrollMode="KeepScrollOffset"
                            RemainingItemsThreshold="1">
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="m:Item">
                        <Frame Style="{StaticResource ItemList_Item}">
                            <Grid ColumnDefinitions="5*,*,*">
                                <Label Style="{StaticResource ItemList_ItemName}"
                                       Text="{Binding Name, Mode=OneTime}"
                                       Grid.Column="0" Grid.Row="0"/>
                            </Grid>
                        </Frame>
                    </DataTemplate>
               </CollectionView.ItemTemplate>
          </CollectionView>
     </Grid>