3
votes

I'm displaying a collection of images in a LongListSelector on WP8 and I've implemented the well known lazy loading pattern utilizing LLS's ItemRealized event.

In the code below OnItemRealized is called for each and every item in the Pictures collection - even for items which are clearly off-screen. In this scenario 24 items fit on screen yet the LLS realizes 40 items and this triggers the ResumeGetPictures() of the ViewModel. When the Pictures collection changes (INotifyCollectionChanged), the LLS will also realize those items until it runs out of items, triggering the next ResumeGetPictures() - this will go until the ViewModel is unable to load more items.

All seems to be fine as long as the LLS is in LayoutMode=List. But when I switch to Grid, the control seems to swallow each and every item in the list and realizes it immediately. Making any sort of lazy loading impossible.

I hope I just did something very very wrong - although I doubt that because I've triple-checked everything and like I said switching to "List" resolves the problem immediately - unfortunately not an option for a photo gallery of some sort.

ViewModel:

public IReactiveDerivedList<TPicture> Pictures
{
  get { return pictures; }
}

View Code-Behind:

lls.ItemRealized += OnItemRealized;

private void OnItemRealized(object sender, ItemRealizationEventArgs e)
{
  var picture = e.Container.Content as Picture;

  if (picture != null)
  {
    // get index
    var pictureIndex = lls.ItemsSource.IndexOf(picture);

    if (pictureIndex >= lls.ItemsSource.Count * 0.95f)
      ViewModel.ResumeGetPictures();
  }
}

XAML:

<phone:LongListSelector Name="lls" Margin="13,-30,0,0"
  ItemsSource="{Binding Pictures}"
  Tap="OnListItemTapped"
  ItemTemplate="{StaticResource ItemTemplate}"           
  IsGroupingEnabled="False"
  LayoutMode="Grid" 
  GridCellSize="108,108"/>
1
This largely depends on the size of the items in your grid, and your way of determining the last item in the list. You should post your code and XAML here.Claus Jørgensen
@ClausJørgensen Code added.Oliver Weichhold
Well, first of all, your multiplication will have no effect as Count is a integer. You might as well been multiplying with one. As for your question, are you saying that OnItemRealized gets called 40 times, if you remove all your logic inside it?Claus Jørgensen

1 Answers

1
votes

I was able to get the desired effect by observing the ScrollBar inside the LLS. I've abstracted the functionality into a behavior for easy re-use:

  public class LLSIncrementalLoadingBehavior : Behavior<LongListSelector>
  {
    private ScrollBar llsScrollBar;

    #region Dependency Properties

    public static readonly DependencyProperty RequestMoreDataProperty = DependencyProperty.Register(
      "RequestMoreData", typeof(Action), typeof(LLSIncrementalLoadingBehavior), new PropertyMetadata(null, OnRequestMoreDataChanged));

    /// <summary>
    /// The action to invoke to initiate loading of more data
    /// </summary>
    public Action RequestMoreData
    {
      get { return (Action) this.GetValue(RequestMoreDataProperty); }
      set { this.SetValue(RequestMoreDataProperty, value); }
    }

    private static void OnRequestMoreDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      ((LLSIncrementalLoadingBehavior)d).RequestMoreData = (Action)e.NewValue;
    }

    public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register(
      "Threshold", typeof(double), typeof(LLSIncrementalLoadingBehavior), new PropertyMetadata(0.8, OnThresholdChanged));

    /// <summary>
    /// A value between 0 and 1 that controls how early more data is requested. Use 1 to only trigger it at the very end
    /// </summary>
    public double Threshold
    {
      get { return (double)this.GetValue(ThresholdProperty); }
      set { this.SetValue(ThresholdProperty, value); }
    }

    private static void OnThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      ((LLSIncrementalLoadingBehavior)d).Threshold = (double)e.NewValue;
    }

    #endregion

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

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
      llsScrollBar = VisualTreeHelperExtensions.FindFirstElementInVisualTree<ScrollBar>(AssociatedObject);

      llsScrollBar.ValueChanged += OnLlsScrollBarValueChanged;
    }

    private void OnLlsScrollBarValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
      var bottomEdge = (float)(e.NewValue + AssociatedObject.ActualHeight);
      var bottom = llsScrollBar.Maximum + AssociatedObject.ActualHeight;
      var threshold = bottom * Threshold;

      if (bottomEdge >= threshold)
        RequestMoreData();
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();

      if (llsScrollBar != null)
      {
        llsScrollBar.ValueChanged -= OnLlsScrollBarValueChanged;
      }
    }
  }

For sake of completeness:

public static T FindFirstElementInVisualTree<T>(DependencyObject parentElement) where T : DependencyObject
{
  if (parentElement != null)
  {
    var count = VisualTreeHelper.GetChildrenCount(parentElement);
    if (count == 0)
      return null;

    for (int i = 0; i < count; i++)
    {
      var child = VisualTreeHelper.GetChild(parentElement, i);

      if (child != null && child is T)
        return (T)child;
      else
      {
        var result = FindFirstElementInVisualTree<T>(child);
        if (result != null)
        {
          return result;
        }
      }
    }
  }
  return null;
}