Paginator: a platform-independent infinite loading component

Paginator: a platform-independent infinite loading component

First a HorizontalListView request

First, being an old sport, I was flattered that a young fellow calls me “mate”.
Second he was right: this list couldn't paginate...

So it was the perfect time for me to introduce a component I reused countless times in many of the projects I worked on (countless and many meaning here 4 and 3, but it's less impressive): THE PAGINATOR.

It's already available in the Sharpnado nuget packages since 0.9.4.

There are 2 things I really like in this component:

  1. Its name: it feels so powerful and dangerous
  2. Its composition vs inheritance philosophy

Me rambling again about composition vs inheritance

Here we go, I already explained a bit here why I think it's better to use composition in the view models than inheritance to implement view model initialization.
But we can also apply this strategy for paging...

Now let's first go back to our full inheritance strategy (and mostly applied).

public abstract class BaseViewModel : INotifyPropertyChanged
{
    ...

    public async void Initialize(object parameter)
    {
        IsBusy = true;
        HasErrors = false;
        try
        {
            await InnerInitializationAsync(parameter);
        }
        catch (Exception exception)
        {
            ExceptionHandler.Handle(exception);
            HasErrors = true;
            ErrorMessage = "this is messy";
        }
        finally
        {
            IsBusy = false;
        }
    }

    protected abstract Task InnerInitializationAsync(object parameter);
}


public abstract class PageableViewModel<T> : BaseViewModel
{
    protect PageableViewModel(int pageSize)
    {
        ...
    }

    public ObservableCollection<T> Items { get; set; }

    protected int PageSize { get; set; }

    protected override Task InnerInitializationAsync(object parameter)
    {
        return LoadPageAsync(1);
    }

    protected Task LoadPageAsync(int pageNumber)
    {
        ...
    }
}

So what's wrong with that ?

  1. You force your view models to fit in a global use case: maybe sometimes you don't need the two features, you want just the pagination or the task loading handling,
  2. It's way less readable: you need to override an abstract method, and your features are hidden in the hierarchy tree,
  3. It feels less natural than composition: if I want to load an async code, I use a ViewModelLoader, if want to paginate data I use a Paginator.

So just like we created a ViewModelLoader component to achieve a nice container for our initialization code, we create a Paginator to handle all the page loading.

Our view models will have the choice to use either one of them or the two combined.

For example, if you already have a large code base implementing the BaseViewModel strategy, you could still use the Paginator in them, that's what components are about, decoupling and reusability.

What is the Paginator?

It's totally platform independant. It's pure netstandard. So you can use it in WPF, Xamarin.Forms, and why not Windows Forms.

It's a portable concept, we could easily translate it into java or the-last-trending-language.

Feature speaking, it's a component easing the pages loading by achieving coordination between manual page loading in the ViewModel, and automatic loading (infinite list) on the Viewside.
The same instance is shared by the ViewModel and the View.
It offers also some fine tuning like specifying the max number of items it can load, and a threshold point of the ListView (or RecyclerView, or UICollectionView, or HorizontalListView, etc...) beyond which a new page will be loaded (thus implementing an infinite list behavior).

It exists as two forms:

  1. A standalone one using only Task (as a Gist here: https://gist.github.com/roubachof/d5b6c64d39ccf4e0adb7859a42533e4b)
  2. A version included in my Sharpnado.Presentation.Forms repo and nuget package.

How the Paginator works

First let's have a look at our implementation in the Sharpnado.Presentation.Forms project: https://github.com/roubachof/Sharpnado.Presentation.Forms/blob/master/Sharpnado.Presentation.Forms/Paging/Paginator.cs.

Although you can use the Paginator anywhere, it's most likely you will create it in the ViewModel, and plug it to your UI Control as a IInfiniteListLoader.

Cause yes, the Paginator bears two responsabilities:

  1. The automatic loading of next pages when scrolling down the UI scrollable component through the OnScroll(int lastVisibleIndex) method
  2. The manual loading of pages in the view model through the LoadPage method

So it breaks SRP... But it makes it powerful cause the infinite loading part can make some decisions based on the actual state of the page loading task. And it's more simple to use since it's the same object but used with a different interface in the UI part.

To ease your understanding, in the following sections we will work on the example of the HorizontalListView used in our Silly! app

The IInfiniteListLoader (the View side)

Basically, you will pass the Paginator as an IInfiniteListLoader to your ListView. There is two places where you can plug it:

  1. On a scrolling event (like RecyclerView.OnScrollListener on Android, Scrolled event of ScrollView on Xamarin.Forms and UICollectionView on iOS, ...)
  2. Directly on the GetCell of the UICollectionViewCell or the OnBindViewHolder of the RecyclerView.Adapter (it seems trash but since the OnScroll method implementation returns on average in less than 10 ticks it won't block your UI thread.)

So in our Xamarin.Forms HorizontalListView, we have this property:

public static readonly BindableProperty InfiniteListLoaderProperty
    = BindableProperty.Create(
        nameof(ItemsSource),
        typeof(IInfiniteListLoader),
        typeof(HorizontalListView));

So our Paginator is set as an IInfiniteListLoader by binding in our list view.

Android implementation

We use the RecyclerView.OnScrollListener to call our component:

private class OnControlScrollChangedListener : RecyclerView.OnScrollListener
{
    ...

    public override void OnScrolled(RecyclerView recyclerView, int dx, int dy)
    {
        base.OnScrolled(recyclerView, dx, dy);

        var infiniteListLoader = _element?.InfiniteListLoader;
        if (infiniteListLoader == null)
        {
            return;
        }

        var linearLayoutManager = (LinearLayoutManager)recyclerView.GetLayoutManager();
        int lastVisibleItem = linearLayoutManager.FindLastVisibleItemPosition();
        if (_lastVisibleItemIndex == lastVisibleItem)
        {
            return;
        }

        _lastVisibleItemIndex = lastVisibleItem;
        InternalLogger.Info($"OnScrolled( lastVisibleItem: {lastVisibleItem} )");
        infiniteListLoader.OnScroll(lastVisibleItem);
    }

iOS implementation

We use here the Scrolled event of our UICollectionView:


private void OnScrolled(object sender, EventArgs e)
{
    var infiniteListLoader = Element?.InfiniteListLoader;
    if (infiniteListLoader != null)
    {
        int lastVisibleIndex =
            Control.IndexPathsForVisibleItems
                .Select(path => path.Row)
                .DefaultIfEmpty(-1)
                .Max();

        if (_lastVisibleItemIndex == lastVisibleIndex)
        {
            return;
        }

        _lastVisibleItemIndex = lastVisibleIndex;

        InternalLogger.Info($"OnScrolled( lastVisibleItem: {lastVisibleIndex} )");
        infiniteListLoader.OnScroll(lastVisibleIndex);
    }

    ...
}

The concrete Paginator (the ViewModel side)

In order to instantiate our Paginator in our view model, we will need some information:

  1. Func<int, int, Task<PageResult<TResult>>> pageSourceLoader
  2. int pageSize = PageSizeDefault
  3. int maxItemCount = MaxItemCountDefault
  4. float loadingThreshold = LoadingThresholdDefault

you can see that only the pageSourceLoader is required, the others parameters have default values.

The pageSourceLoader

It's a Func taking a page number, a page count, and returning a PageResult<TResult>:

public struct PageResult<TItem>
{
    public static readonly PageResult<TItem> Empty = new PageResult<TItem>(0, new List<TItem>());

    public PageResult(int totalCount, IReadOnlyList<TItem> items)
    {
        TotalCount = totalCount;
        Items = items ?? new List<TItem>();
    }

    public int TotalCount { get; }

    public IReadOnlyList<TItem> Items { get; }
}

In most of the cases, your TItem will be the type of your list items. So in fact your page source loader will:

  1. Call REST service
  2. Create ViemModel items object from Model objects
  3. Add them to your ObservableRangeCollection
  4. Fill and return the PageResult

Remark: TItem could also be the type of your model objects, in that case you will use the optional OnTaskCompleted callback to transform the models into view models and raise "property changed" on your collection. It can be helpful if you need to create different view models from these models later.

The others arguments

  • The pageSize is self explanatory, if you don't know what it is, please come back later :),
  • The maxItemCount is the maximum item count that the paginator will load. If the count is reached, the OnScroll method will not load any more pages,
  • The loadingThreshold is more related to the UI layer, it will specify the point on your UI control beyond which the next page loading will be triggered. It's a percentage from the current pageSize. From the doc: Let's say you have 40 items loaded in your List and page size is 10, if the threshold is 0.5, the loading of the next page will be triggered when element 35 will become visible. If threshold is 0.25, it will be triggered at element 37.

Jeez enough gibberish man show me a concrete example

Let's get back to our beacon in the night, our home sweet home, our "you'll never walk alone", our Silly! app.

In the following example we will learn to use our ViewModelLoader with the Paginator.

The SillyInfinitePeopleVm view model

Let's have a look at the SillyInfinitePeopleVm, and first its constructor.

public class SillyInfinitePeopleVm : ANavigableViewModel
{
    private const int PageSize = 10;
    private readonly ISillyDudeService _sillyDudeService;

    public SillyInfinitePeopleVm(
        INavigationService navigationService,
        ISillyDudeService sillyDudeService,
        ErrorEmulator errorEmulator)
        : base(navigationService)
    {
        _sillyDudeService = sillyDudeService;

        ...

        SillyPeople = new ObservableRangeCollection<SillyDudeVmo>();

        SillyPeoplePaginator = new Paginator<SillyDude>(
            LoadSillyPeoplePageAsync,
            pageSize: PageSize,
            loadingThreshold: 1f);

        SillyPeopleLoader = new ViewModelLoader<IReadOnlyCollection<SillyDude>>(
            ApplicationExceptions.ToString,
            SillyResources.Empty_Screen);
    }

    public ViewModelLoader<IReadOnlyCollection<SillyDude>>
        SillyPeopleLoader { get; }

    public Paginator<SillyDude>
        SillyPeoplePaginator { get; }

    public ObservableRangeCollection<SillyDudeVmo>
        SillyPeople { get; set; }

    ...
}

We are interested in these 3 properties that will be coordinated:

  1. SillyPeopleLoader
  2. SillyPeoplePaginator
  3. SillyPeople

The paginator is instantiated with the data source LoadSillyPeoplePageAsync, the pageSize will be 10, and the loadingThreshold 1 (which means that as soon as the first item of the last page is shown, a new page is loaded).

Let's have a look to the data source of our paginator:

private async Task<PageResult<SillyDude>> LoadSillyPeoplePageAsync(
    int pageNumber, int pageSize)
{
    PageResult<SillyDude> resultPage =
        await _sillyDudeService.GetSillyPeoplePage(pageNumber, pageSize);

    var viewModels = resultPage.Items
        .Select(dude => new SillyDudeVmo(dude, GoToSillyDudeCommand))
        .ToList();

    SillyPeople.AddRange(viewModels);

    return resultPage;
}

So the paginator will use this method to automatically add new pages through the OnScroll method, or manually with the LoadPage.

Now the interesting part, let's see how our ViewModelLoader interacts with the Paginator. Let's go to the Load which is called when we navigate for the first time to the view model:

private void Load()
{
    SillyPeople = new ObservableRangeCollection<SillyDudeVmo>();
    RaisePropertyChanged(nameof(SillyPeople));

    SillyPeopleLoader.Load(async () =>
    {
        ...

        return (await SillyPeoplePaginator.LoadPage(1)).Items;
    });
}

The signature of our ViewModelLoader Load method (TData is here IReadOnlyCollection<SillyDude>):

void Load(Func<Task<IReadOnlyCollection<SillyDude>>> loadingTaskSource, bool isRefreshing = false)

Now remember the ViewModelLoader is in charge of the initialization of our view model. In our semantic case, the initialization is the loading of the first page of our list of silly dudes. So we just call manually LoadPage(1) which will return the PageResult for our first page. It will handle all the cases such as error, empty state, etc...

And that's it for our view model.

The SillyInfinitePeoplePage view

For the View part, our HorizontalListView already supports IInfiniteListLoader, then it's just about setting a binding in our SillyInfinitePeoplePage:

...

<renderedViews:HorizontalListView Grid.Row="3"
    Margin="-16,8"
    CollectionPadding="0,8"
    EnableDragAndDrop="True"
    InfiniteListLoader="{Binding SillyPeoplePaginator}"
    ItemHeight="144"
    ItemSpacing="8"
    ItemTemplate="{StaticResource DudeTemplateSelector}"
    ItemWidth="144"
    ItemsSource="{Binding SillyPeople}"
    SnapStyle="Center" />

...

Thanks

Thanks for following me throughout this kinda long post for such a simple component. From my point of view, it enlights well some common pitfall we meet in our day to day MVVM developer life.

Don't forget to checkout embedded documentation on the standalone paginator and the Sharpnado.Presentation.Forms. It's quite verbose ;)