Incremental ListView Control and Plugin for Xamarin.Forms

0 Comments

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