Procrastination FTW - LazyListBox Should Improve your Scrolling Performance and Responsiveness

Procrastination FTW - LazyListBox Should Improve your Scrolling Performance and Responsiveness

Rate This
  • Comments 9

Introduction

One of the things I've been working on for a while is a "smart" ListBox implementation that uses the new scrolling states and data virtualization features in Windows Phone 7 to help improve performance. Here's a relatively common pattern we see with developers who are new to Silverlight (or just new to the phone):

  1. Get a very large dataset of complex objects, possibly coming from a web request
  2. Bind it to a ListBox with a complex ItemTemplate that has an expensive layout, often including images
  3. Witness an unresponsive / glitchy application during page-load or while scrolling

Sometimes Step 3 isn't as noticeable on the emulator because it is very fast on a powerful desktop computer, but on the resource-constrained phone you are more likely to see issues with performance and responsiveness when binding large, complex lists. This post includes a sample application that shows three different ways of binding to data from the Netflix OData service (which is very cool, by the way!) and how each improves on the former.

The first part of the problem (binding to a large dataset) can be mitigated by using IList instead of IEnumerable, as discussed in my data virtualization posting and in Shawn's eerily-similar post. The next part of the problem - what to do about those expensive ItemTemplates and the other heavy lifting that is required to actually show your items - is what this post is focused on mitigating. You should also read the recently-posted article on ListBox performance over at the newly-minted Silverlight for Windows Phone Performance team blog.

A Three-Step Solution

The solution outlined in this post has three parts to it, each of which I'll discuss in turn. They are:

  • Using the scrolling state to avoid doing work while the list is scrolling
  • Using different DataTemplates for your items, depending on whether they are actually on-screen or not
  • Using an explicit state machine for controlling the work done by items depending on whether they are visible or not

Let's look at each in turn, although they are all related.

Don't do work while the user is scrolling

The first part of the solution is to use the scroll-states exposed by the ScrollViewer in Windows Phone to detect when the list is scrolling and when it has stopped scrolling. There are two reasons for doing this. The first is that you want to avoid doing any work on the UI thread while the list is being scrolled, otherwise it won't be responsive to the user's gestures or you might see blank items in your list if the UI thread (which is creating the content for the items) can't keep up with the render thread (which is animating them). The other is that you want to avoid doing any work for items that are not visible to the user (see the next section) but the computation for what is visible and what is not visible is expensive, and per the previous sentence we don't want to do that expensive work while the list is scrolling. So we need to wait for the list to stop scrolling before we can compute the visible items.

The IsScrolling property and ScrollingStateChanged event

The LazyListBox exposes a DependencyProperty named IsScrolling that will tell you whether or not the list is scrolling; you can databind to it if you like, for example to show or hide some other UI in your application. It also exposes a ScrollingStateChanged event that you can hook to do other work when the state changes. The property and the event are synthesized by hooking the VisualStateGroup.CurrentStateChanging event of the underlying ScrollViewer, as outlined in my previous blog post on the topic. Internally, the LazyListBox also uses the scrolling start notification to alert the items in the list that they should "Pause" any work they are currently doing, and it uses the stop notification to compute the visible items in the list.

The OnListChangesComplete method

The most interesting code in the sample is probably LazyListBox.OnListChangesComplete and the functions that it calls. In particular, it uses some extension methods and some LINQ magic to check which items are visible in the list using the extension method ExtensionMethods.GetVisibleItems<T>(). This method in turn ends up calling TestVisibility for each of the virtualized items in the list (which is expensive, which is why it only happens when you stop scrolling). Basically the whole reason I need to detect list scrolling state is so that I can avoid calling OnListChangesComplete unless I know that the list changes (and animations) are, well, complete.

TestVisibility is the "expensive" work-horse, and it looks like this:

public static bool TestVisibility(this FrameworkElement item, FrameworkElement viewport, Orientation orientation, bool wantVisible)
{
 
// Determine the bounding box of the item relative to the viewport
  GeneralTransform transform = item.TransformToVisual(viewport);
  Point topLeft = transform.Transform(new Point(0, 0));
  Point bottomRight = transform.Transform(new Point(item.ActualWidth, item.ActualHeight));

  // Check for overlapping bounding box of the item vs. the viewport, depending on orientation
  double min, max, testMin, testMax;
  if (orientation == Orientation.Vertical)
  {
    min = topLeft.Y;
    max = bottomRight.Y;
    testMin = 0;
    testMax = Math.Min(viewport.ActualHeight, double.IsNaN(viewport.Height) ? double.PositiveInfinity : viewport.Height);
  }
  else
  {
    min = topLeft.X;
    max = bottomRight.X;
    testMin = 0;
    testMax = Math.Min(viewport.ActualWidth, double.IsNaN(viewport.Width) ? double.PositiveInfinity : viewport.Width);
  }

  bool result = wantVisible;

  if (min >= testMax || max <= testMin)
    result = !wantVisible;

  return result;
}

In theory this method could be sped up by accumulating the maximum Y (or X) value and then assuming that each subsequent item is directly below (to the right of) the previous item, but that didn't seem worth the extra effort.

Don't compute complex layouts if the items aren't on-screen

Once we know which items are on-screen and which items are not, we can do interesting things like only show complex DataTemplates for the items the user can see. Then we don't pay the cost of databinding and laying out all the items that you can't see anyway. In the sample, I use three different DataTemplates for the LazyListBoxItem container (which are set via the LazyListBox properties):

  1. ItemTemplate. This is the normal template that is exposed by ListBox and is used by default for all items in the list when they are not on-screen. This should be a very simple template that databinds only the bare minimum of data (if any!) and doesn't have a complex layout
  2. LoadedItemTemplate. This is a new template that is used only when items are on-screen, and this is where you can show the full, rich UI that is databound to various properties and has a complex layout (but not too complex!)
  3. CachedItemTemplate. This is an "advanced" template used when an item has been previously-visible but is now invisible; it is used only if your items implement ILazyDataItem (see next section)

The selection of which template to use is done in LazyListBoxItem.SetIsVisible(), which is a function that OnListChangesComplete ends up calling for all the virtualized items in the list to let them know if they are currently visible or not. 

Animating data into view

Using LoadedItemTemplate along with some animations can lead to some nice effects, like the one used in the Netflix sample project. If you haven't run the project yet, the ItemTemplate looks like this:

netflix_itemtemplate

And the LoadedItemTemplate looks like this:

netflix_loadeditemtemplate

The app plays a simple fade animation between the two of them when the list comes to a halt - here's the XAML, with the interesting bits highlighted (I wish copying colour-coded text from VS worked correctly... I manually formatted the code above, but XML is just too hard to format by hand):

<lazy:LazyListBox x:Name="MainListBox" Margin="0,0,-12,0" Opacity="0.3" ItemsSource="{Binding Data}">
 
<lazy:LazyListBox.ItemTemplate>
    <DataTemplate>
     
<Border Height="100">
        <TextBlock Text="{Binding Title}" Style='{StaticResource PhoneTextLargeStyle}' Margin='112,1,12,0' Grid.Column='1' VerticalAlignment='Center' />
      </Border>
    </DataTemplate>
  </lazy:LazyListBox.ItemTemplate>
 
<lazy:LazyListBox.LoadedItemTemplate>
    <DataTemplate>
      <Grid Height="100">
        <Grid.Triggers>
          <!--Show a nice animation to fade the minimal content into the full content-->
         
<EventTrigger RoutedEvent="Grid.Loaded">
            <BeginStoryboard>
              <BeginStoryboard.Storyboard>
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="largeText" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:0.6">
                    <DoubleAnimation.EasingFunction>
                      <PowerEase EasingMode="EaseIn" Power="3"/>
                    </DoubleAnimation.EasingFunction>
                  </DoubleAnimation>
                  <DoubleAnimation Storyboard.TargetName="details" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.6">
                    <DoubleAnimation.EasingFunction>
                      <PowerEase EasingMode="EaseOut" Power="3"/>
                    </DoubleAnimation.EasingFunction>
                  </DoubleAnimation>
                </Storyboard>
              </BeginStoryboard.Storyboard>
            </BeginStoryboard>
          </EventTrigger>
        </Grid.Triggers>
       
<Grid.ColumnDefinitions>
          <ColumnDefinition Width="100"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Image Source="{Binding ImageSource}" Grid.Column="0" Stretch="Uniform" Width="65" Height="90" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="12,0"/>
        <TextBlock x:Name="largeText" Grid.Column="1" Text="{Binding Title}" Style='{StaticResource PhoneTextLargeStyle}' VerticalAlignment='Center'/>
        <StackPanel Margin="1,4,0,0" Grid.Column="1" x:Name="details" Height="80" VerticalAlignment="Top">
          <TextBlock Text="{Binding Title}" Style='{StaticResource PhoneTextNormalStyle}' Height='27'/>
          <TextBlock Text="{Binding Description}" TextWrapping="Wrap" Height="53" FontSize="{StaticResource PhoneFontSizeSmall}" Style='{StaticResource PhoneTextSubtleStyle}'/>
        </StackPanel>
      </Grid>
    </DataTemplate
>
  </lazy:LazyListBox.LoadedItemTemplate>
</lazy:LazyListBox>

It's pretty straight-forward stuff - the blue text is the simple ItemTemplate, the maroon stuff is the LoadedItemTemplate, and the green stuff is the (one and only) supported Trigger in Silverlight that I am using to do the fade-in animation (you could use the ControlStoryboardAction from Blend instead if you liked). Note that my ItemTemplate is very simple and consists of just a TextBlock inside a Border, whereas the LoadedItemTemplate contains a Grid with two columns, an Image, three TextBlocks, and an animation.

The net effect is that when you're scrolling the list you see the nice big title text (and nothing else), and when you stop it fades away to show you the full details including the picture. Not only does this aid with performance, but it also makes the list contents more readable as you are scrolling because the text is larger. Neat,huh?

Delay all work until the last-possible minute

The final thing we do is to not do any work at all! The system tries to avoid doing work whenever it doesn't need to, and you can also participate in this lazy behaviour if you implement the ILazyDataItem interface on your items that are added to the list.

ILazyDataItem interface

This interface is used to implement a state machine that models the various states a "heavy" object can be in. It models the data loaded into the object on two independency axes:

  1. The speed with which the data can be retrieved (synchronous or asynchronous)
  2. The size of the data (basically "small strings" or "large objects")

If the LazyListBoxItem class is home to one of these objects, it will use it and follow the state machine below (click to enlarge):

statemachine

It looks complicated ;-) but it's actually quite simple. The common state transitions are:

  • Unloaded -> Minimum. You load the bare minimum content to display something on the screen (to match your ItemTemplate)
  • Minimum -> Loading. Time to start loading all the data for your item (to match your LoadedItemTemplate)
  • Loading -> Loaded. You have finished loading your items (this is a transition driven by you, not by LazyListBoxItem)
  • Loaded -> Cached. You were loaded, but now you're off-screen and should dump any memory-hogging data (to match your CachedItemTemplate)

There is also a Reloading state which is just a special case of Loading (when you already have some data loaded), and you can optionally support a Pause / UnPause operation to stop doing work while the list is scrolling.

In the interests of time, I did not implement ILazyDataItem in the Netflix demo (otherwise I would never have gotten this post out!) but it is used by the dummy data item in the DelayLoadListBoxItem project.

Other miscellaneous stuff

The project has some other things in it, like remembering the scroll position of a list during tombstoning (using the extension method SetVerticalScrollOffset) which turned out to be incredibly hard to get right due to the mysterious ways in which events come in, especially when databinding. It took me many, many hours of trial-and-error debugging and required a custom interface (ISupportOffsetChanges) to implement it correctly and in a way that didn't perform more work than was necessary... I really hope that it actually is a hard problem (all that work was necessary) vs. it being an obvious thing and I just wasted my time :-).

That's about it for now - hope you enjoy the code and can use it in your apps. There are several things in the ZIP file attached:

  • UIExtensionMethods. Implements various extension methods for dealing with the visual tree, etc.
  • LazyListBox. Implementation the LazyListBox and LazyListBoxItem classes, and defines the ILazyDataItem interface
  • DelayLoadListBoxItem. A test project that implements the ILazyDataItem interface and arbitrarily sucks up memory. More of a testing app that I used while developing the LazyListBox than anything else
  • NetflixBrowserTest. A test project I build after writing the LazyListBox to see the real-world difference in performance and to see how hard (or not) it would be to implement a "clean room" application that used the lazy list box.
  • State Machine for ILazyDataItem. A PowerPoint deck that contains the state machine image, above, for your reference
Attachment: LazyListBoxBlogPost.zip
  • Thanks Peter, I'm excited to check this out. I tried using David Anson's DeferredLoadListBox and was still having some issues, even tried using the SL Performance techniques outlined above. I will give this a shot and use the simpler template while scrolling, should work nicely I think.

    Oh one other thing, any news on the TransitioningContentControl you blogged about a while back? I was using it for a while and then I found those exceptions you mentioned (pressing Back while animation is in progress). It was very unfortunate since I was about to submit my app for approval, now I'm going back and hard-coding transitions in each page. An update or some guidance on nice page transitions would be extremely beneficial to the community I think -- snapping page transitions just won't do :)

    -Matt

  • Hi Matt, this solution still isn't perfect (at the end of the day, your app is doing work) but I hope it can help you. As for the animations, we're working on getting something out soon.

  • Can you give any pointers about using this in the following scenario?  Panorama -> Panorama Item (x5) -> User Control with LazyLists?

    Your example code does some opacity setting and other changes based on when the data is loaded but imagine a setting where the data is loaded once (on a main page) and the user controls just update based on binding.  How would you know when to have the user control stop showing the "Loading..."?

    Thanks

  • Hi Surfer Mikel, not sure I understand your scenario completely. If the data is already loaded by your main page, you don't need to wait for it to load again (?). Anyway if you want to show a "Loading..." message until the data has loaded, you want to do something like raise an event when loading has completed (or rely on INotifyPropertychanged, or maybe an ObservableCollection).

  • Dear Ptorr:

    I had try your LazyListbox, and it works great.

    But I have a issue need your suggestions.

    I am going to  implement a online photo album application.

    I need a LazyListbox for photos' Thumbnail.

    How can I put multi LazyListBox.Items in one Column?

    use WrapPanel or ?

    Thank a million.

  • Hello Peter,

    I have a question: suppose i am not interested in applying different templates to the UI controls that are not currently visible on the screen. If so, isn't this the same as the UI virtualization performed by a regular ListBox's underlying VirtualStackPanel (except of course your additional functionality of pausing the work when still scrolling, together with ILazyDataItem) ? If i understood correctly when reading about UI virtualization in ListBoxes, the controls that are not currently visible on the screen are not rendered, nor their layouts are computed - except maybe for some of the immediately preceding and following ones - so there's no need to worry about them.

  • Gabriel: Yes, the standard ListBox will virtualize the UI but if you have complex layouts or or you are binding to images they you will likely see glitches and unresponsive UI.

  • This is absolutely a life-saver. It's a little concerning that this needs to exist at all, however. I understand we are dealing with the constraints of a mobile device, but I really hope either performance is improved to the point where this isn't necesary, or at very least this control gets put in the official toolkit. Thanks for all your hard work! I don't think I could release without this.

  • Glad you found it useful Gary. Yes, perf is something we are looking at for future releases :-)

Page 1 of 1 (9 items)