[Xamarin.Forms] Behavior para hacer scroll infinito

stack-04-wfIntroducción

Actualmente el control de tipo listado incluido en Xamarin.Forms incluye soporte a características avanzadas interesantes como el uso de plantillas diferentes para cada elemento del listado o la posibilidad de realizar Pull To Refresh.

A pesar de que el refresco de listados realizando Pull To Refresh es una forma bastante habitual y extendida, no es la única. También en ocasiones es necesario refrescar listados a la medida que se va realizando scroll, lo que se conoce como scroll infinito.

¿Cómo realizamos scroll infinito en Xamarin.Forms?

El evento ItemAppearing

El control ListView lanza el evento ItemAppearing cada vez que un elemento pasa a ser visible. Nuestra tarea para detectar por lo tanto si hemos llegado al final es que el último elemento visible es el último elemento disponible.

NOTA: Habitualmente siempre que sea posible se recomienda el uso de idntificadores para realizar las comprobaciones necesarias.

Creando un Behavior

Los Behaviors (o comportamientos en Español) nos permiten añadir lógica directamente en XAML para realizar acciones sin necesidad de escribir código extra en el code behind.

Vamos a crear un Behavior que asociado a un listado permita lanzar un comando llegado al final del scroll para solicitar nuevos elementos.

Comenzamos creando una clase que herede de Behavior<T>:

public class IniniteListScrollingBehavior : Behavior<ListView>
{

}

Debemos implementar:

  • Propiedad AssociatedObject: Control al que se adjuntará el Behavior, es decir, el ListView.
  • Método OnAttachedTo: Lanzado inmediatamente tras adjuntar el Behavior al control. Se recibe una referencia al control adjuntado idóneo para acceder a propiedades o eventos del mismo.
  • Método OnDetachingFrom: Lanzado cuando el Behavior se elimina del control. Lugar perfecto para realizar tareas de limpieza (gestión de suscripciones de eventos, liberar recursos, etc.).
public ListView AssociatedObject { get; private set; }

protected override void OnAttachedTo(ListView bindable)
{
     base.OnAttachedTo(bindable);

     AssociatedObject = bindable;
     bindable.BindingContextChanged += OnBindingContextChanged;
     bindable.ItemAppearing += OnItemAppearing;
}

protected override void OnDetachingFrom(ListView bindable)
{
     base.OnDetachingFrom(bindable);

     bindable.BindingContextChanged -= OnBindingContextChanged;
     bindable.ItemAppearing -= OnItemAppearing;
     AssociatedObject = null;
}

private void OnBindingContextChanged(object sender, System.EventArgs e)
{
     base.OnBindingContextChanged();
     BindingContext = AssociatedObject.BindingContext;
}

private void OnItemAppearing(object sender, ItemVisibilityEventArgs e)
{
     var listview = ((ListView)sender);

     if (listview.IsRefreshing)
          return;
}

Vamos a utilizar el método ItemAppearing del ListView para lanzar un comando encargado de refrescar el listado. Necesitamos crear una BindableProperty de tipo ICommand en el Behavior:

public static readonly BindableProperty CommandProperty =
     BindableProperty.Create("Command", typeof(ICommand), typeof(IniniteListScrollingBehavior), null);

public ICommand Command
{
     get { return (ICommand)GetValue(CommandProperty); }
     set { SetValue(CommandProperty, value); }
}

En el método ItemAppearing del control:

private void OnItemAppearing(object sender, ItemVisibilityEventArgs e)
{
     var listview = ((ListView)sender);

     if (listview.IsRefreshing)
          return;

     if (Command == null)
     {
          return;
     }

     if (Command.CanExecute(e.Item))
     {
          Command.Execute(e.Item);
     }
}

Accedemos al comando (siempre y cuando el listado no siga refrescando) y lo ejecutamos pasándole el último elemento visible como parámetro. Este parámetro lo utilizaremos desde la ViewModel para determinar si ese elemento es el último de la lista o no.

Utilizando el Behavior

Tras crear el Behavior ha llegado el momento de utilizarlo. Pero antes de lanzarnos de pleno…¿de dónde obtenemos la información?.

En la ViewModel tendremos una propiedad pública con el listado a mostrar:

private ObservableCollection<Monkey> _monkeys;

public ObservableCollection<Monkey> Monkeys
{
     get { return _monkeys; }
     set
     {
          _monkeys = value;
          RaisePropertyChanged();
     }
}

Utilizaremos también un par de propiedades para determinar:

  • IsBusy: Esta propiedad nos indicará cuando se esta realizando la carga de más información. Utilizada en la UI para mostrar un indicador visual de carga en caso necesario.
  • CurrentPage: Un entero que almacena el número de página utilizado al cargar información. Gran cantidad de APIs soportan paginación. En la mayoría de ocasiones necesitamos indicar el número de página a cargar (justo esta propiedad) y el número de elementos por página.
public bool IsBusy { get; set; }

public int CurrentPage { get; set; }

En nuestro ejemplo, para simplificar todo en la medida de lo posible y centrarnos en el uso del Behavior, vamos a cargar datos locales:

private void LoadItems(int pageSize = 10)
{
     IsBusy = true;

     if(Monkeys == null)
     {
          Monkeys = new ObservableCollection<Monkey>();
     }

     for (int i = CurrentPage; i < CurrentPage + pageSize; i++)
     {
          Monkeys.Add(new Monkey()
          {
               MonkeyId = i + 1,
               Name = string.Format("Monkey {0}", i + 1)
          });
     }

     CurrentPage = Monkeys.Count;
     IsBusy = false;
}

El Behavior utilizará un comando para refrescar la información:

private ICommand _refreshCommand;

public ICommand RefreshCommand
{
     get { return _refreshCommand = _refreshCommand ?? new DelegateCommand<Monkey>(RefreshCommandExecute, RefreshCommandCanExecute); }
}

public bool RefreshCommandCanExecute(Monkey monkey)
{
     return !IsBusy && 
            Monkeys.Count != 0 && 
            Monkeys.Last().MonkeyId == monkey.MonkeyId;
}

public void RefreshCommandExecute(Monkey monkey)
{
     LoadItems();
}

Para utilizarlo en nuestra UI compartida XAML, debemos crear un namespace:

xmlns:behavior="clr-namespace:InfiniteScrollingBehavior.Behaviors;assembly=InfiniteScrollingBehavior"

Utilizando la propiedad Behaviors del listado, adjuntamos el Behavior creado enlazando con el comando previamente visto.

<ListView
     ItemsSource="{Binding Monkeys}"
     IsRefreshing="{Binding IsBusy}"
     HasUnevenRows="true">
     <ListView.ItemTemplate>
          <DataTemplate>
               <ViewCell>
                    <templates:MonkeyTemplate />
               </ViewCell>
           </DataTemplate>
       </ListView.ItemTemplate>
       <ListView.Behaviors>          
          <behavior:IniniteListScrollingBehavior  
                Command="{Binding RefreshCommand}">
          </behavior:IniniteListScrollingBehavior>
     </ListView.Behaviors>
</ListView>

El resultado:

inifinitescrollingbehavior

Sencillo y además facilmente reutilizable.

Tenéis el código fuente del ejemplo utilizado disponible en GitHub:

Ver GitHub

Recuerda, cualquier tipo de duda o sugerencia es bienvenida en los comentario del artículo.

Más información

Deja un comentario

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