UWP Grouping IncrementalLoadingList

MahStudio 1 41 Reputation points
2020-05-20T20:43:35.333+00:00

Hi there everyone.
I have a quick question but I didn't find any solution for it.
Is there any way to create an IncrementalLoadingList with Grouping feature?
I want to create a view in my app that used for messaging and then I need to get messages and group them by date (May 21, May 20, ..... December 31 2019 and etc.)

Is there any way for doing that?

Universal Windows Platform (UWP)
0 comments No comments
{count} votes

Accepted answer
  1. Matt Lacey 791 Reputation points MVP
    2020-05-21T00:16:34.827+00:00

    Assuming that the data is returned in groups already (if it's not you couldn't do incremental loading) there are two options.

    If you don't want sticky headers
    Add the data you want as a group header into the list with a property that indicates it's a group header, then use a data template selector to alter the styling for header items.

    If you do want sticky headers:
    As above but with lots more code and debugging depending on your UI skills.


1 additional answer

Sort by: Most helpful
  1. Daniele 1,996 Reputation points
    2020-05-22T16:03:45.753+00:00

    To group items you can use CollectionViewSource and bind its View property to a ListView.ItemsSource, that allow you to have a ListView that shows element with sticky headers (you can enable or disable sticky headers). You have to bind to CollectionViewSource.Source a collection of collections object, that means you have to group data by yourself.

    To have incremental loading you need to create a class that implement ISupportIncrementalLoading.

    The point here is that if you bind a class that implements ISupportIncrementalLoading to CollectionViewSource.Source the method LoadMoreItemsAsync is not automatically invoked, and here you have to write some code.

    For example assume you have a class to hold your grouped data like that, a collection of strings:

    public class StringGroup : ObservableCollection<string>
    {
        public readonly string Key;
    
        public StringGroup(string key)
        {
            Key = key;
        }
    }
    

    To implement incremental loading and have a list of StringGroup create a class that both extends a collection of and implements ISupportIncrementalLoading similar to IncremetalStringGroups defined below. IncremetalStringGroups holds your grouped data and his LoadMoreItemsAsync, at each iteration, create random strings grouped by the first letter.

    public class IncremetalStringGroups : ObservableCollection<StringGroup>, ISupportIncrementalLoading
    {
        private readonly Random _random = new Random();
    
        private char _nextLetter = 'A';
    
        public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
        {
            var stringGroup = new StringGroup(_nextLetter.ToString());
            int countToAdd = _random.Next(5, 10);
            for (var counter = 0; counter < countToAdd; counter++)
            {
                stringGroup.Add($"{_nextLetter} {counter}");
            }
            Add(stringGroup);
    
            _nextLetter = (char) (_nextLetter + 1);
            return AsyncInfo.Run(ct => Task.FromResult(new LoadMoreItemsResult {Count = (uint) Count}));
        }
    
        public bool HasMoreItems => _nextLetter == 'z' + 1;
    }
    

    To make it simple for the example, declare and instantiate IncremetalStringGroups in your Page code behind

    private readonly IncremetalStringGroups _availableGroups;
    [...]
    _availableGroups = new IncremetalStringGroups();
    

    create CollectionViewSource as a resource of your Page (note x:Name and not x:Key) bound to _availableGroups

    <Page.Resources>
        [...]
        <CollectionViewSource x:Name="CollectionViewSource" IsSourceGrouped="True" Source="{x:Bind _availableGroups}"/>
        [...]
    </Page.Resources>
    

    and the ListView boud to the CollectionViewSource

    <ListView ItemsSource="{x:Bind CollectionViewSource.View}" x:Name="IncrementalGroupedListView">
        [...]
    </ListView>
    

    Now you have to add the code that calls LoadMoreItemsAsync method of IncremetalStringGroups. You need to call LoadMoreItemsAsync

    • the first time when ListView is loaded
    • some more times after ListView loading to fill the available space
    • when you scroll
    • when you change the size of the ListView

    For that I used events ListView.Loaded, ItemsStackPanel.LayoutUpdated, ScrollViewer.ViewChanged and ItemsStackPanel.SizeChanged creating class IncrementalGroupedListViewHelper that you can instantiate in this way in the Page constructor.

    new IncrementalGroupedListViewHelper(IncrementalGroupedListView, _availableGroups);
    

    Here below IncrementalGroupedListViewHelper definition, I added comments in the code to explain the steps, hope that they are enough clear.

    public class IncrementalGroupedListViewHelper
    {
        private readonly ListView _listView;
        private readonly ISupportIncrementalLoading _supportIncrementalLoading;
    
        private ScrollViewer _scrollViewer;
        private ItemsStackPanel _itemsStackPanel;
    
        public IncrementalGroupedListViewHelper(ListView listView, ISupportIncrementalLoading supportIncrementalLoading)
        {
            _listView = listView;
            _supportIncrementalLoading = supportIncrementalLoading;
            _listView.Loaded += ListViewOnLoaded;
        }
    
        private async void ListViewOnLoaded(object sender, RoutedEventArgs e)
        {
            if (!(_listView.ItemsPanelRoot is ItemsStackPanel itemsStackPanel)) return;
    
            _itemsStackPanel = itemsStackPanel;
    
            _scrollViewer = VisualTreeHelperUtils.Child<ScrollViewer>(_listView);
    
            // This event handler loads more items when scrolling.
            _scrollViewer.ViewChanged += async (o, eventArgs) =>
            {
                if (eventArgs.IsIntermediate) return;
                double distanceFromBottom = itemsStackPanel.ActualHeight - _scrollViewer.VerticalOffset - _scrollViewer.ActualHeight;
                if (distanceFromBottom < 10) // 10 is an arbitrary number
                {
                    await LoadMoreItemsAsync(itemsStackPanel);
                }
            };
    
            // This event handler loads more items when size is changed and there is more
            // room in ListView.
            itemsStackPanel.SizeChanged += async (o, eventArgs) =>
            {
                if (itemsStackPanel.ActualHeight <= _scrollViewer.ActualHeight)
                {
                    await LoadMoreItemsAsync(itemsStackPanel);
                }
            };
    
            await LoadMoreItemsAsync(itemsStackPanel);
        }
    
        private async Task LoadMoreItemsAsync(ItemsStackPanel itemsStackPanel)
        {
            if (!_supportIncrementalLoading.HasMoreItems) return;
            // This is to handle the case when the InternalLoadMoreItemsAsync
            // does not fill the entire space of the ListView.
            // This event is needed untill the desired size of itemsStackPanel 
            // is less then the available space.
            itemsStackPanel.LayoutUpdated += OnLayoutUpdated;
            await InternalLoadMoreItemsAsync();
        }
    
        private async void OnLayoutUpdated(object sender, object e)
        {
            if (_itemsStackPanel.DesiredSize.Height <= _scrollViewer.ActualHeight)
            {
                await InternalLoadMoreItemsAsync();
            }
            else
            {
                _itemsStackPanel.LayoutUpdated -= OnLayoutUpdated;
            }
        }
    
        private async Task InternalLoadMoreItemsAsync()
        {
            await _supportIncrementalLoading.LoadMoreItemsAsync(5); // 5 is an arbitrary number
        }
    }
    

    This is to give the general idea, maybe something has to be refined. Hope this helps.

    1 person found this answer helpful.
    0 comments No comments