The blog of dlaa.me

Never do today what you can put off till tomorrow [DeferredLoadListBox (and StackPanel) help Windows Phone 7 lists scroll smoothly and consistently]

In my previous post about how LowProfileImageLoader helps the Windows Phone 7 UI thread stay responsive by loading images in the background, I began with the following introduction:

When writing applications for small, resource-constrained scenarios, it's not always easy to balance the demands of an attractive UI with the requirements of fast, functional design. Ideally, coding things the natural way will "just work" (the so-called pit of success) - and it usually does. But for those times when things need some extra "oomph", it's nice to have options. That's what my latest project, PhonePerformance is all about: giving developers performance-focused alternatives for common Windows Phone 7 scenarios.

 

In this second post, I'll be demonstrating the use of my DeferredLoadListBox class in conjunction with the Silverlight StackPanel in order to get good performance from a scenario that's pretty common for social media applications: a scrolling list of entries with a picture and a brief bit of text. (Ex: Twitter posts, Facebook updates, blog comments, and the like.) As usual, I've written a sample application to show what I'm talking about - it displays a simple list of about 200 image+name pairs from the web (the followers of my Twitter account [you know who you are! :) ]). You can see a screen shot of the "List Scrolling" sample below:

PhonePerformance List Scrolling sample
Note: Everything in this post pertains to the latest (internal) Windows Phone 7 builds, not the public Beta bits. Although I'd expect things to work the same on the Beta build, I haven't verified that because the final bits will be released to the public on September 16th.
Additional note: My discussion assumes all testing is done on actual phone hardware. Although running the emulator on a PC is a fantastic development experience, performance of applications in the emulator can vary significantly. The emulator is great for writing new code and fixing bugs, but performance work should be done on a real phone if at all possible.

 

Okay, go ahead and run the sample and wait a moment for it to load the user list from the web (and enable those two buttons). Then choose the "default" (left) or "performance" (right) scenario and watch the behavior of the little blue dots moving across the screen as the list content populates. (I discussed the motivation behind the blue dots in the previous post - for now just remember that when the dots are flowing smoothly, life is good - and when the dots get jumpy or stop animating completely, the application is unresponsive.)

Right now, we're interested in the different load times of the two scenarios. The "performance" side uses a StackPanel to get good scroll performance, but making that switch without doing anything else is likely to increase load times versus the default VirtualizingStackPanel used by the Windows Phone ListBox (because of the loss of virtualization; more on this later). That's why the sample also uses DeferredLoadListBox - to offset the performance loss by re-introducing enough pseudo-virtualization to bring performance back up to where it was. And as the sample application shows, the load times of the two lists are very similar. (Granted, neither is instantaneous - but it's also not the case that one is consistently faster than the other.)

Now that both lists are populated, the real experiment begins: go ahead and scroll the "default" (left) list up and down and watch the item content as you do so. When scrolling at slow or moderate speeds, the movement will be smooth and the experience will be great. But once you start scrolling quickly, things begin to break down - you may start to see brief glimpses of missing items, flickering, or even black-outs...

So with that baseline experience in mind, it's time to scroll the "performance" (right) list to see how it compares. The first thing you'll probably notice is that it's slower to load the images - that's due to the use of LowProfileImageLoader and was the topic of my previous post. The next thing you'll probably notice is that the scrolling is smooth for just about any speed - and especially for content that's already been on the screen at least once! I won't claim scrolling is perfect with StackPanel and DeferredLoadListBox, but it has been my experience (and that of others) that it can be notably better than the default behavior.

 

To understand why the DeferredLoadListBox+StackPanel combination is effective, it's necessary to understand a little about how VirtualizingStackPanel works - and why that ends up being a problem in this scenario...

Pretty much every Panel implementation for Silverlight and WPF works by measuring and arranging all its elements according to various layout guidelines (ex: stack, grid, etc.). This makes it straightforward to implement a custom Panel, but it also means that all the Children must be created and live in the visual tree from the beginning. Because in the vast majority of cases there are only a handful of children (and they're all on screen anyway), this isn't a problem. However, in the "really long list" scenario, there are often only 5-10 items on the screen at a time - and a few hundred other items that aren't. VirtualizingStackPanel works in conjunction with ItemsControl and ItemContainerGenerator to create only those elements that are actually on the screen. As the user scrolls the list, VirtualizingStackPanel automatically creates containers for any items about to come into view and recycles the containers for items that just scrolled out of view. (In reality, there's usually a screen's worth of buffer on either side.) Consequently, there are two big wins with VirtualizingStackPanel: load time (by virtue of creating only a fraction of the total elements) and memory consumption (by virtue of keeping only a fraction of the total elements around at any time).

But (as sometimes happens in life) VirtualizingStackPanel's strength is also its weakness. All that container recycling and item juggling takes time to execute - and that's precious time on the UI thread that can't be spent doing other things (like updating the UI). So what seems to happen when a list is scrolling quickly enough is that the VirtualizingStackPanel falls behind more and more - until some of those containers that are scrolling into view are blank because they haven't been created or populated yet! And that's when the user sees visual glitches and rendering issues...

Therefore, the fundamental approach I've taken to avoiding problems in scenarios like this is to replace the ListBox's VirtualizingStackPanel ItemsPanel with a StackPanel. This is quite easy to do - and the StackPanel's non-virtualizing nature means that scrolling long lists will be smooth as silk. However, there are two downsides to making this switch: load time is considerably longer and memory use will be significantly higher. The increased memory consumption probably won't be an issue with small- or medium-sized lists, but it could start to be a problem for large lists containing thousands of items. But while there are options for mitigating this, that's not my objective and I won't be going into them here. (Besides, I'm not sure how practical it is for people to scroll super-long lists, anyway!)

The way I avoid longer load times is by using the DeferredLoadListBox class I wrote for just this purpose - what it does is hold off on populating off-screen containers until they're about to show up on the screen. In this manner, it restores some of the benefits of VirtualizingStackPanel by making the relevant portion of the load time (roughly) independent of the total number of items in the list. But it's important to note that DeferredLoadListBox works in one direction only! Although there's no reason it couldn't "re-virtualize" items as they scroll out of view, it specifically doesn't do so because doing (and undoing) that would consume precious CPU cycles. DeferredLoadListBox is really only about softening the blow of switching from VirtualizingStackPanel to StackPanel - it's not about trying to re-implement VirtualizingStackPanel's fundamental behavior.

So with scrolling glitches avoided and load time back to where it started, there's just one other thing slowing things down and detracting from the user experience: the jumpiness that takes place during the first full scroll of the list. What's going on there is that the first full scroll causes all those "pseudo-virtualized" containers to be created - which causes the corresponding images to be downloaded from the web. We've established that downloading images on the UI thread is bad for performance, and this is exactly the scenario I created LowProfileImageLoader for! So by throwing LowProfileImageLoader into the mix, the first full scroll stays responsive, too.

And at this point, we've arrived at the smooth, pleasing, consistent scrolling experience you get from the "List Scrolling" sample's "performance" column! Yay us! :)

 

Of course, every application - and every scenario - is different, so there's no guarantee StackPanel, DeferredLoadListBox, or LowProfileImageLoader will help all the time (or even most of the time!). But what's nice is that it's extremely easy to try them out (alone or together), so there's nothing to lose by trying them out if your scenario seems likely to benefit!

 

[Click here to download the compiled PhonePerformance assembly, sample applications, and full source code for everything.]

 

To show what I mean, here's what the default scenario looks like:

<ListBox ItemsSource="{Binding Followers}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid Height="50">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="50"/>
                    <ColumnDefinition Width="10"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Rectangle
                    Grid.Column="0"
                    Fill="{StaticResource PhoneChromeBrush}"/
                <Image
                    Grid.Column="0"
                    Source="{Binding ProfileImageUrl}"
                    Width="48"
                    Height="48"/>
                <TextBlock
                    Grid.Column="2"
                    Text="{Binding ScreenName}"
                    VerticalAlignment="Center"/>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="Height" Value="50"/>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

And here's all it takes to convert it to use StackPanel, DeferredLoadListBox, and LowProfileImageLoader:

<delay:DeferredLoadListBox ItemsSource="{Binding Followers}">
    <delay:DeferredLoadListBox.ItemTemplate>
        <DataTemplate>
            <Grid Height="50">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="50"/>
                    <ColumnDefinition Width="10"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Rectangle
                    Grid.Column="0"
                    Fill="{StaticResource PhoneChromeBrush}"/>
                <Image
                    Grid.Column="0"
                    delay:LowProfileImageLoader.UriSource="{Binding ProfileImageUrl}"
                    Width="48"
                    Height="48"/>
                <TextBlock
                    Grid.Column="2"
                    Text="{Binding ScreenName}"
                    VerticalAlignment="Center"/>
            </Grid>
        </DataTemplate>
    </delay:DeferredLoadListBox.ItemTemplate>
    <delay:DeferredLoadListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="Height" Value="50"/>
        </Style>
    </delay:DeferredLoadListBox.ItemContainerStyle>
    <delay:DeferredLoadListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel/>
        </ItemsPanelTemplate>
    </delay:DeferredLoadListBox.ItemsPanel>
</delay:DeferredLoadListBox>

(Don't forget to add the appropriate XMLNS to the top of the XAML file:)

xmlns:delay="clr-namespace:Delay;assembly=PhonePerformance"

 

The only thing I haven't talked about yet is the requirement that each ListBoxItem container needs to have a fixed height (via the ItemContainerStyle property; see above). This is currently necessary because it enables some optimizations in DeferredLoadListBox - however, it's important to note there's no need for all the containers to have the same fixed height - just that they all need to have a fixed height.

 

Aside: As it exists today, DeferredLoadListBox only works with vertical scrolling lists. Of course, the same concepts can be applied to horizontally scrolling lists, too, but because that's not consistent with the UI conventions of Windows Phone 7, I haven't tried to generalize the code to support both orientations. I optimized for the performance of the common scenario - but if you'd like to tweak things to support horizontal scrolling instead - or as well! - it should be fairly straightforward to do. :)