Download Code: LoadAsYouScroll.zip

Load As You Scroll

What if we could change objects by just looking at them?



Might be handy (?) Surprisingly, that is precisely what WPF controls can do with SyncUI, and AsyncUI properties, because such properties load on Value.get(). And that is handy.
The above patterns feature OddStrategy, hence their characteristic behavior:
  • Every odd Value.get() call - the loading is started, current value is returned;
  • Every even Value.get() call - the value is returned.
They are both based on AutoPropertyBase abstraction, and that is to say:
  • ReloadCommand is executed - PropertyChanged is raised for Value;
  • A new value is loaded - PropertyChanged is raised for Value.
This way all the pieces fall into place. Every odd time the data binding mechanism of the control will call Value.get() of the property and initiate the loading process, each even time the binding mechanism will call the same accessor and collect the results. This process can be induced through the use of the property's ReloadCommand. When the command is called the Value is invalidated and that causes a new request from the view.

So the properties will respond to the requests from the view. But how do we send the requests and more importantly where do we place the code for that?
The answer to this questions can certainly be useful outside the bounds of PF and here it is:

We'll design a behavior that will keep an eye on the scroll viewer of our control and execute a command when it is scrolled to the end. Consider the following implementation:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace LoadAsYouScroll
{
    public class FireWhenScrolledBehaviour : Behavior<ItemsControl>
    {
        #region Command

        public static readonly DependencyProperty CommandProperty =
             DependencyProperty.Register("Command", typeof(ICommand),
             typeof(FireWhenScrolledBehaviour));

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

        #endregion Command

        private ScrollViewer ScrollViewerState;

        protected override void OnAttached()
        {
            AssociatedObject.Loaded += OnLoaded;
            AssociatedObject.Unloaded += OnUnLoaded;
        }

        protected override void OnDetaching()
        {
            AssociatedObject.Loaded -= OnLoaded;
            AssociatedObject.Unloaded -= OnUnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Decorator border = VisualTreeHelper.GetChild(AssociatedObject, 0) as Decorator;
            ScrollViewerState = border.Child as ScrollViewer;

            ScrollViewerState.ScrollChanged += ScrollViewerState_ScrollChanged;
            // Have to call it now. The data may load before the behavior is attached.
            ScrollViewerState_ScrollChanged(this, null);
        }

        private void OnUnLoaded(object sender, RoutedEventArgs e)
        {
            ScrollViewerState.ScrollChanged -= ScrollViewerState_ScrollChanged;
        }

        void ScrollViewerState_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            if (ScrollViewerState.ContentVerticalOffset > 
         ScrollViewerState.ExtentHeight - ScrollViewerState.ViewportHeight-1)
            {
                if (Command.CanExecute(null)) Command.Execute(null);
            }
        }   
    }
}

Lots of code, but arguably it's worth it. Now we shall only need another couple of lines in our XAML code to get the requests logic running.

<ListView ItemsSource="{Binding Numbers.List.Value}">
    <i:Interaction.Behaviors>
        <local:FireWhenScrolledBehaviour Command="{Binding Numbers.List.ReloadCommand}"/>
    </i:Interaction.Behaviors>
</ListView>

Easy as pie and clear as the blue sky. Now to the view model part. Here the whole loading mechanism is encapsulated to produce a single CollectionViewSource-like object that uses a function returning pages by page numbers.

public class MainViewModel
{
    public MainViewModel()
    {
        _Numbers = new ScrollList<string>(_Service.GetStringData);
    }

    private readonly ScrollList<string> _Numbers;
    public ScrollList<string> Numbers { get { return _Numbers; } }
//...
}

The implementation of ScrollList too emphasizes simplicity.

// Implements Load As You Scroll pattern
public class ScrollList<T> 
{
    /// <param name="getPage">Function that accepts the page number
    /// and returns the page data</param>
    public ScrollList(Func<int,IEnumerable<T>> getPage)
    {
        _List = RxProperty.AsyncUI<IEnumerable<T>>(o => 
            _AllLoaded.Value ? o : o.Concat(_NewPage.Value))
            .Default(Enumerable.Empty<T>())
            .Command(rel => new DelegateCommand(_ => rel(), _ => !_List.IsLoading && !_AllLoaded.Value));

        _NewPage = SyncProperty.SyncGet<IEnumerable<T>>(_ => getPage(_PagesLoaded.Value))
            .Default(Enumerable.Empty<T>())
            .TriggersReload(o => o.Any() ? _PagesLoaded : _AllLoaded as IConditionallyReloadable);

        _PagesLoaded = SyncProperty.SyncCommand<int>(o => o + 1);
        _AllLoaded = SyncProperty.SyncCommand<bool>(_ => true);
    }

    private readonly IAsyncCommandProperty<IEnumerable<T>> _List;
    public IAsyncCommandProperty<IEnumerable<T>> List { get { return _List; } }

    private readonly ISyncProperty<IEnumerable<T>> _NewPage;

    private readonly ISyncCommandProperty<int> _PagesLoaded;
    private readonly ISyncCommandProperty<bool> _AllLoaded;
}

I don't really mean to spoil the pleasure of exploring the code on your own, but here are a few notes aimed to help you understand it faster if you'd like to:
  • _List uses a custom command to stop the client code from starting the loading logic unnecessarily (when loading is in process or all pages have already been loaded);
  • When _NewPage loads an empty page it signals the end of the list. Otherwise it increments the page count;
  • Both _List and _NewPage are set to empty IEnumerables by default. The method Default(...) sets the initial value of a property and doesn't raise PropertyChanged;
  • SyncGet properties use DirectStrategy and are targeted at being used in view models. AsyncGet, SyncUI, and AsyncUI properties use OddStrategy and are designed to communicate directly with view controls.
Ok. Enough words!

It's time to play with this stuff, and of course, there's no better way to do that than to download the code and see it in person.

Have fun and good luck!

Last edited Jul 3, 2013 at 11:07 PM by Vitaly_Kamiansky, version 7

Comments

No comments yet.