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.