This week I'll introduce you to a new control I've wrapped into a "plugin" for Xamarin.Forms - IncrementalListView
. This control allows you to specify a PageSize
and PreloadCount
to control when and how many items to load as a user scrolls. It's powerful and easy to drop into your project. Take a look at the end result:
ISupportIncrementalLoading
The plugin also includes an interface that a ViewModel should inherit from and implement. I've modeled this after the Windows ISupportIncrementalLoading interface. This provides a contract for the control to use while also making it easier to test the ViewModel loading items incrementally.
public interface ISupportIncrementalLoading
{
int PageSize { get; set; }
bool HasMoreItems { get; set; }
bool IsLoadingIncrementally { get; set; }
ICommand LoadMoreItemsCommand { get; set; }
}
Implementing this is simple. Let's take a look at an example.
public class IncrementalViewModel : INotifyPropertyChanged, ISupportIncrementalLoading
{
public int PageSize { get; set; } = 20;
public ICommand LoadMoreItemsCommand { get; set; }
public bool IsLoadingIncrementally { get; set; }
public bool HasMoreItems { get; set; }
public IncrementalViewModel()
{
LoadMoreItemsCommand = new Command(async () => await LoadMoreItems());
}
async Task LoadMoreItems()
{
IsLoadingIncrementally = true;
// Download data from a service, etc.
// Add the newly download data to a collection
HasMoreItems = ...
IsLoadingIncrementally = false;
}
}
I've set a PageSize
to 20
, which is something I can use in my LoadMoreItems
method for retrieving data in a paginated way. The LoadMoreItems
command will be executed as we scroll the IncrementalListView
and reach the PreloadCount
, which we will talk about shortly. Finally, we update HasMoreItems
to reflect if there is more data to get or not. Note, for brevity, I have left out any INotifyPropertyChanged
implementations you might want here.
IncrementalListView
The next step is to use the control in a page. Here is an example.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:IncrementalListView.FormsPlugin.Abstractions;assembly=IncrementalListView.FormsPlugin.Abstractions"
x:Class="IncrementalListViewSample.IncrementalListViewPage" Padding="0,20,0,0">
<local:IncrementalListView
ItemsSource="{Binding MyItems}"
PreloadCount="5"
RowHeight="88">
<x:Arguments>
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
</x:Arguments>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding .}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Footer>
<ActivityIndicator IsRunning="{Binding IsLoadingIncrementally}" IsVisible="{Binding IsLoadingIncrementally}"/>
</ListView.Footer>
</local:IncrementalListView>
</ContentPage>
There is not much different from a standard ListView
except a new property, PreloadCount
. This value indicates how maybe rows before the end of the list to start loading. In this example, once we hit MyItems.Count - 5
then, the LoadMoreItemsCommand
will be executed. This is an optimization to help reduce the amount of time a user has to stare at the ActivityIndicator
while scrolling. Hopefully, we can start loading more as a user gets close to the end of the list so they don't have to wait at all, or at least wait less time.
How does it work?
I started down the path of using a renderer for each platform only to hit a wall on Android because ListViewAdapter
is internal
. I looked for another way to hook into ListView
to customize this behavior and found the ItemAppearing event. I was customizing the GetCell
method of a UITableViewSource
originally, so this seemed like a similar cross-platform way instead.
public class IncrementalListView : ListView
{
...
public IncrementalListView(ListViewCachingStrategy cachingStrategy)
: base(cachingStrategy)
{
ItemAppearing += OnItemAppearing;
}
void OnItemAppearing(object sender, ItemVisibilityEventArgs e)
{
int position = itemsSource?.IndexOf(e.Item) ?? 0;
if (itemsSource != null)
{
// preloadIndex should never end up to be equal to itemsSource.Count otherwise
// LoadMoreItems would not be called
if (PreloadCount <= 0)
PreloadCount = 1;
int preloadIndex = Math.Max(itemsSource.Count - PreloadCount, 0);
if ((position > lastPosition || (position == itemsSource.Count - 1)) && (position >= preloadIndex))
{
lastPosition = position;
if (!incrementalLoading.IsLoadingIncrementally && !IsRefreshing && incrementalLoading.HasMoreItems)
{
LoadMoreItems();
}
}
}
}
void LoadMoreItems()
{
var command = incrementalLoading.LoadMoreItemsCommand;
if (command != null && command.CanExecute(null))
command.Execute(null);
}
}
Incrementally loading from Azure App Services
As a bonus item, I thought it would be fun to show how I used this with a data source. Azure App Services made it really easy using the IMobileServiceClient
. Here is an example in my AzureDataService
class.
public async Task<PaginatedResult<T>> GetPaginatedDataAsync<T>(
int fetchOffset, int fetchMax = 10,
Expression<Func<T, bool>> predicate = null,
CancellationToken cancellationToken = default(CancellationToken))
where T : class
{
// Check before we get started
cancellationToken.ThrowIfCancellationRequested();
var table = mobileServiceClient.GetTable<T>();
List<T> results = new List<T>();
if(predicate != null)
results = await table.Skip(fetchOffset)
.Take(fetchMax)
.Where(predicate)
.IncludeTotalCount()
.ToListAsync()
.ConfigureAwait(false);
else
results = await table.Skip(fetchOffset)
.Take(fetchMax)
.IncludeTotalCount()
.ToListAsync()
.ConfigureAwait(false);
var totalCountEnumerable = results as IQueryResultEnumerable<T>;
long totalCount = totalCountEnumerable.TotalCount;
var result = new PaginatedResult<T>
{
Results = results,
TotalCount = totalCount
};
return result;
}
Here, you see that I cast my results to IQueryResultEnumerable<T>
so that I can get the TotalCount
. This information let's me know if there are more records that I will need to load, which I can use back in my LoadMoreItems
method to set HasMoreItems
. I would use it like this:
items = await azureDataService.GetPaginatedDataAsync(MyItems.Count, PageSize);
We've taken advantage of the support for Skip
and Take
to only get a "page" of data from my Azure tables.
That's all there is too it! Now we have a cross-platform ListView
that supports incremental loading and we've seen how to use it with Azure App Services. Let me know if you have any feedback or thoughts on how we can make this even better. Take a browse through the full source and sample, available on my GitHub page. I've also made this available as a NuGet package on NuGet.
Comments