Infinite Scrolling with Incremental Loading in Windows Phone 8

March 17th 2013 Windows Phone MVVM

In mobile applications there's often a need for scrolling lists with large or even (for all practical purposes) infinite amounts of data which of course needs to be loaded incrementally. The canonical example for this is Twitter timeline, but any other long lists of posts such as RSS feeds, log entries etc. call for a similar implementation, as long as they provide an API for loading data in batches, i.e. it is possible to request a range of results between two indices.

Official support for implementing such a list is already available since Windows Phone 7.1 (Mango) release, although it still seems too much of hassle to me. Implementing such a basic functionality shouldn't require modification of default ScrollViewer style and handling VisualStates in code behind.

Things got better with LongListSelector control, distributed with Windows Phone Toolkit for Windows Phone 7 and included in Windows Phone 8 SDK. Now it's enough to handle either Link or ItemRealized event, depending on whether you're using Windows Phone 7 or Windows Phone 8.

Still, you probably want to avoid this when using MVVM pattern and instead have a command being called when additional data needs to be loaded. That's exactly what the following behavior does:

public class IncrementalLoadingBehavior : Behavior<LongListSelector>
{
    public static readonly DependencyProperty LoadCommandProperty =
        DependencyProperty.Register("LoadCommand",
            typeof (ICommand),
            typeof (IncrementalLoadingBehavior),
            new PropertyMetadata(default(ICommand)));

    public ICommand LoadCommand
    {
        get { return (ICommand) GetValue(LoadCommandProperty); }
        set { SetValue(LoadCommandProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.ItemRealized += OnItemRealized;
    }

    private void OnItemRealized(object sender, ItemRealizationEventArgs e)
    {
        var longListSelector = sender as LongListSelector;
        if (longListSelector == null)
        {
            return;
        }

        var item = e.Container.Content;
        var items = longListSelector.ItemsSource;
        var index = items.IndexOf(item);

        if ((items.Count - index <= 1) && 
            (LoadCommand != null) && 
            (LoadCommand.CanExecute(null)))
        {
            LoadCommand.Execute(null);
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.ItemRealized -= OnItemRealized;
    }
}

You can use it by simply adding it to the LongListSelector's Behaviors collection:

<i:Interaction.Behaviors>
    <b:IncrementalLoadingBehavior LoadCommand="{Binding LoadCommand}" />
</i:Interaction.Behaviors>

Inside LoadCommand's Execute method you just load the next batch of data. You can even change the return value of CanExecute method based on whether there's any more data to load and thus prevent Execute from even being called.

Since I decided to use Caliburn.Micro as the MVVM framework in my latest Windows Phone project this solution turned out not to be really tailored to it. Caliburn prefers actions to commands for invoking methods in view models. Ideally this means no additional code needs to be written for connecting control events to view model methods. There's even special syntax available to make this work:

<phone:LongListSelector 
    cal:Message.Attach="[Event ItemRealized]=[Action LoadMoreItems($eventArgs)]" />

Unfortunately this requires the logic for deciding between loading additional items or not to be moved to the view model - for each individual case of implementing this functionality. Additionally while testing this approach I noticed the view model method wasn't really invoked for the first dozen of items or so (not really sure why, I assumed Caliburn's messaging wasn't initialized in time) which could introduce subtle bugs preventing further data to be loaded in some cases. A custom trigger turned out to be optimal solution addressing both of the concerns:

public class IncrementalLoadingTrigger : TriggerBase<LongListSelector>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.ItemRealized += OnItemRealized;
    }

    private void OnItemRealized(object sender, ItemRealizationEventArgs e)
    {
        var longListSelector = sender as LongListSelector;
        if (longListSelector == null)
        {
            return;
        }

        var item = e.Container.Content;
        var items = longListSelector.ItemsSource;
        var index = items.IndexOf(item);

        if (items.Count - index <= 1)
        {
            InvokeActions(null);
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.ItemRealized -= OnItemRealized;
    }
}

Similar to a behavior this trigger can be used by simply adding it to LongListSelector's Triggers collection:

<i:Interaction.Triggers>
    <t:IncrementalLoadingTrigger>
        <cal:ActionMessage MethodName="LoadMoreItems"/>
    </t:IncrementalLoadingTrigger>
</i:Interaction.Triggers>

The only thing that needs to be added to the view model is LoadMoreItems method, which belongs there anyway.

For a full working example application check out this post.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License