[Windows 8.1] Listas paginadas

Hola a todos!

Hoy vamos a cambiar un poco de temática, para hablar de paginación en apps Windows Store para Windows 8.1.

En Windows Phone 8, usamos la capacidad del LongListSelector para detectar el final de la lista y poder lanzar la carga de más elementos. En Windows Store no disponemos de LongListSelector. En su lugar usamos un control GridView o ListView. Estos controles no tienen la capacidad de detectar la compresión final para cargar más elementos. En su lugar, realizan la carga de más páginas automáticamente usando la interface ISupportIncrementalLoading.

Esta interface debe estar implementada en la colección que enlazamos al GridView o ListView. Por defecto no existe ninguna colección que la implemente por lo que tendremos que crearnos la nuestra propia. Para ello podemos partir de una ObservableCollection. ISupportIncrementalLoading implementa una propiedad de solo lectura llamada HasMoreItems y un método llamado LoadMoreItemsAsync:

ISupportIncrementalLoading
public class PaginatedCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
{

    public bool HasMoreItems
    {
        get { throw new NotImplementedException(); }
    }

    public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    {
        throw new NotImplementedException();
    }
}

Normalmente necesitaremos usar este tipo de colección con más de un tipo de dato y la carga de más páginas, en el método LoadMoreItemsAsync, también variará. Para solventar esto podemos usar una táctica parecida a la empleada en el DelegateCommand en MVVM. Vamos a añadir un constructor que se encargue de recibir dos Func<T>: Uno para saber si debemos cargar más elementos y otro para cargarlos:

PaginatedCollection ctor
Func<uint, IEnumerable<T>> getMoreItems;
Func<bool> getHasMoreItems;

public PaginatedCollection(Func<uint, IEnumerable<T>> _getMoreItems, Func<bool> _getHasMoreItems)
{
    getMoreItems = _getMoreItems;
    getHasMoreItems = _getHasMoreItems;
}

Ahora simplemente, la propiedad HasMoreItems llamará a getHasMoreItems:

HasMoreItems
public bool HasMoreItems
{
    get
    {
        return getHasMoreItems();
    }
}

En el método LoadMoreItemsAsync, tendremos que trabajar un poco más. Básicamente debemos realizar los siguientes pasos:

  1. Llamar a getMoreItems
  2. Insertar los elementos nuevos en la colección base
  3. Devolver un nuevo LoadMoreItemsResult con el conteo de elementos recibidos.

Pero existen ciertas peculiaridades. El método LoadMoreItemsAsync devuelve un tipo IAsyncOperation<LoadMoreItemsResul>. Tendremos que ejecutar nuestro código en una Task para luego convertirlo a IAsyncOperation. Pero al ejecutar el código en una Task, si intentamos añadir elementos a la colección base obtendremos una excepción cross thread. Para evitar esta excepción, antes de modificar el método LoadMoreItemsAsync, vamos a añadir una propiedad pública y estática a nuestra clase App, para poder exponer el Frame de la aplicación y acceder fácilmente al Dispatcher que contiene, de forma que podamos usar el hilo principal de UI para añadir elementos a la colección. ¿Porqué? Pues por que la base de nuestra colección es una ObservableCollection, que notifica a la UI cada vez que su contenido cambia (al añadir o eliminar un elemento) y esta notificación debe producirse desde el hilo principal de interface de usuario:

Static RootFrame
sealed partial class App : Application
{
    public static Frame RootFrame { get; set; }

Una vez hecho esto, ya podemos escribir el código del método LoadMoreItemsAsync:

LoadMoreItemsAsync
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
    return Task.Run<LoadMoreItemsResult>(async () =>
    {
        var newItems = getMoreItems(count);

        await App.RootFrame.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
        () =>
        {
            foreach (var item in newItems)
                base.Add(item);
        });

        return new LoadMoreItemsResult() { Count = (uint)newItems.Count() };
    }).AsAsyncOperation<LoadMoreItemsResult>();
}

En este código, lo primero que hacemos es devolver una ejecución de una Task, como una AsyncOperation de LoadMoreItemsResult. Internamente dentro de esta Task, llamamos al Func<uint, IEnumerable<T>> que recibimos en el constructor, getMoreItems, pasándole el conteo de elementos que nos pide la interface de usuario. Esto puede ser muy útil si nuestra petición de nuevos datos permite indicar cuantos elementos obtener.

Una vez que recibimos los elementos, usamos el método RunAsync del dispatcher incluido en el RootFrame para, mediante un foreach, añadir cada elemento devuelto a la colección base. Por último la tarea devuelve un nuevo LoadMoreItemsResult, con el número de elementos devueltos.

Ya tenemos el código de nuestra clase PaginatedCollection<T> terminado. Ahora llega el momento de usarla en una ViewModel. En primer lugar creamos una variable privada y una propiedad pública, como con cualquier otra lista:

Public property use
private PaginatedCollection<string> collection;
public PaginatedCollection<string> Collection
{
    get { return collection; }
    set
    {
        collection = value;
        RaisePropertyChanged();
    }
}

A continuación vamos a escribir dos métodos privados, uno para devolver true/false y otro para obtener nuevos elementos. Deben coincidir con la firma que hemos declarado en la colección: Func<bool> y Func<uint, IEnumerable<T>> respectivamente:

HasMoreItems & GetMoreItems
private bool HasMoreItem()
{
    if (elementCount > 100)
        return false;

    return true;
}

private IEnumerable<string> GetMoreItems(uint count)
{
    IList<string> list = new List<string>();

    for (int i = 0; i < count; i++)
    {
        list.Add(string.Format(«string number {0}», i));
    }
    elementCount += count;

    return list;
}

En este ejemplo, para no complicarlo, no estamos llamando a ningún servicio, simplemente cada vez que se nos piden datos, creamos nuevas strings. Exactamente el número que nos pide la lista. mientras ese número esté por debajo de 100, seguimos devolviendo true en nuestro método boolean. Por último, en nuestro constructor, inicializamos la nueva colección:

Collection constructor
public MainViewModel()
{
    collection = new PaginatedCollection<string>(GetMoreItems, HasMoreItem);
}

Y ahora solo nos queda el XAML. Para este ejemplo usaremos un GridView. También podríamos usar un ListView, que soporta paginación.

GridView XAML
<GridView ItemsSource=»{Binding Collection}«
            IncrementalLoadingThreshold=«1»
            IncrementalLoadingTrigger=«Edge»>
    <GridView.ItemTemplate>
        <DataTemplate>
            <Grid Margin=«24» Width=«350»>
                <TextBlock Text=»{Binding}« FontSize=«36»/>
            </Grid>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

Como podemos ver no existe nada especial en esta declaración del GridView, salvo dos propiedades: IncrementalLoadingThreshold e IncrementalLoadingTrigger. Por defecto IncrementalLoadingTrigger tiene el valor Edge, la he indicado explícitamente para mostrarla, pero no es necesario hacerlo. IncrementalLoadingThreshold es muy importante sin embargo. Indica el número de páginas pendientes de ver antes de pedir nuevas. Cuidado, el número de páginas. Si introducimos un valor de 1, la petición de nuevos datos no se realizará hasta que el usuario no llegue casi al final de la lista. si introducimos 10, dejaremos mucho espacio entre el final y el usuario. Debemos calibrar esta propiedad bien, para equilibrar la respuesta al usuario con el uso de recursos.

Y ya tenemos terminado nuestro código. Si ejecutamos, veremos que funciona perfectamente. Ahora, una pregunta para nota. ¿Donde se realiza la carga inicial de datos? En ningún momento hemos llamado para obtener datos inicialmente. Esta es una de las ventajas del sistema de paginación. No tenemos que realizar la carga inicial. Cuando el control se enlaza, ve que tiene 0 elementos y como está dentro del Threshold indicado, pide datos a la lista inmediatamente. Con lo que nos hemos ahorrado tener que inicializarla nosotros mismos.

Con esto llegamos al final de este artículo. Espero que os haya gustado y sea útil. Como siempre, aquí tenéis el código de ejemplo en funcionamiento para que podáis jugar con él y tenerlo de referencia.

Un saludo y Happy Coding!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *