A useful feature for most data-bound items presenting controls is the ability to page their data, by showing only the first n elements and requesting the rest on demand as the users scrolls down the view. This is specially useful when downloading each item is costly, and waiting until the whole collection is obtained to show the user the result demands too much patience from him.
So I decided to implement this on Silverlight, as a way of continuing my old post on data binding, which ended with an unfulfilled promise of a sequel.
I won't be creating a DataTemplate for this example, so I just override the ToString() method to display something human-friendly.
Next thing to do was creating a read-only wrapper for a Client collection, that implemented ICollection<Client>. The point was returning a custom iterator that just wrapped the underlying collection but also logged every action performed by the control.
Nothing too fancy as you see. The test consisted in binding both a DataGrid and a ListBox to this collection, and check which items were requested initially, and which ones as I scrolled.
The results with the ListBox were predictable. It contains a panel that handles the layout of its items, and a ScrollViewer that controls which part of the panel is visible. This panel "believes" that it has all the space it needs to render, and the ScrollViewer draws a rectangular window (the viewport) that moves through it. Therefore, the ListBox makes a single initial call requesting all elements and displaying them. Scrolling makes no change to the already loaded items collection.
However, the results for the DataGrid were most surprising. It did implement some sort of paging, since requests to the collection were made as I scrolled down (although many repeated requests were made). Anyway, even if with repetitions it would be possible to make it work, there was also the initial sweep through the entire collection.
Considering that both controls did the same initial sweep which I had to avoid, and the ListBox (being much more lightweight than the DataGrid) contained all the functionality I needed, I decided to go and try to implement paging on listboxes.
Step one was detecting when the user scrolled down to the bottom of the ListBox, to fire an event requesting for another page. This proved much more difficult than I expected.
The ListBox itself doesn't expose any property regarding its visible items, it is therefore necessary to obtain its internal ScrollViewer. As I was extending the ListBox control (remember to set DefaultStyleKey in the constructor when you extend from a control!), I had access to the protected GetTemplateChild(name) method, which searches for a control with the specified name inside the control's current template. And thanks to the Silverlight convention on elements' names in a styled control's visual tree, we know that the ScrollViewer is (or should be) always named "ScrollViewer" (remember that it was "ScrollViewer Element" in beta 1). The LayoutUpdated event of the control is a nice spot to obtain the reference.
Now there is full access to the VerticalOffset property of the ScrollViewer, which returns the position of the viewport. The problem is Silverlight offers no way to listen to changes in a dependency property you have not declared. You can only specify the OnChange callback when you register it. Note that if you are working in WPF, it is possible to do this with the DependencyPropertyInfo class.
There are some ugly things you can do. There is always the possibility to attach with a two-way binding to the chosen property (actually, you would only need one-way-to-source, but it is only available in WPF) and listen to the setter of a certain property in a mock data context. But this doesn't work if the dependency property you are trying to listen to is read-only, as the Binding object will try to assign to it the value in the DataContext when it is first attached, and throw an exception. Needless to say, VerticalOffset is read only.
Going with reflector inside the ScrollViewer, I found there is a useful ScrollChanged event, but it is internal to the System.Windows.Controls assembly. Extending ScrollViewer and re-writing most of its functionality to gain access to this event is not an option, since it is a sealed class, and ListBox requires that it the control used as a ScrollViewer must be a, well, ScrollViewer.
So, I had to do something like this to finally attach to the Scroll event:
The only trick here is that the ValueChanged event fires multiple times whenever you scroll (reason unknown), and it is important to ensure that only one page is requested when the user scrolls to the bottom. This is easily solved by manually keeping track of the last value received.
The final step here is actually appending the new page to the list box wherever you handle the event. The easiest way of doing this is by using an ObservableCollection<T> so the ListBox automatically adds any items added to the underlying data source. Just add the items to the collection and the scroll will shrink and move slightly up, indicating that new elements have been added.
That is the only issue I have found so far with this implementation: it looks too much like SQL's view of a table, where it shows a limited amount of rows, and when you scroll down new rows are added and the scrollbar's thumb shrinks. It is a wise idea to show the user the total amount of items somewhere, so it knows whether the scrolling will eventually end or he may spend his whole life requesting additional pages.