Paging in Data-Bound ListBox in Silverlight
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.
First thing to do was check how controls (DataGrid and ListBox) behave when bound to a collection. I created a mock class to populate the controls with:
public class Client { public String Name { get; set; } public int Age { get; set; } public bool Vip { get; set; } public override string ToString() { return String.Format("Client {0} {1}", Name, Age); } }
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.
public class ClientsEnumerator : IEnumerator<Client> { public string Name { get; private set; } public int Count { get; private set; } public int Index { get; private set; } private Client[] clients; public ClientsEnumerator(string name, Client[] clients) { Debug.WriteLine("Enumerator {0} created.", name); this.Name = name; this.Count = clients.Length; this.clients = clients; } #region IEnumerator<Client> Members public Client Current { get { Debug.WriteLine("Enumerator {0} requested item {1}.", Name, Index); return clients[Index]; } } #endregion #region IDisposable Members public void Dispose() { Debug.WriteLine("Enumerator {0} disposed.", Name); } #endregion #region IEnumerator Members object System.Collections.IEnumerator.Current { get { Debug.WriteLine("Enumerator {0} requested item {1}.", Name, Index); return clients[Index]; } } public bool MoveNext() { Index++; Debug.WriteLine("Enumerator {0} moved to {1}.", Name, Index); return (Index < Count); } public void Reset() { Index = 0; Debug.WriteLine("Enumerator {0} reset to {1}.", Name, Index); } #endregion }
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.
Luckily, this issue had already been discussed in the Silverlight forums (which I strongly recommend to read whenever you find a problem) and was solved: it is not the ListBox, neither the ScrollViewer you have to attach to to get the Scroll event. It is the ScrollBar.
So, I had to do something like this to finally attach to the Scroll event:
void PagingListBox_LayoutUpdated(object sender, EventArgs e) { ScrollViewer viewer = base.GetTemplateChild("ScrollViewer") as ScrollViewer; FrameworkElement viewerRoot = (FrameworkElement)VisualTreeHelper.GetChild(viewer, 0); vertical = ((ScrollBar)viewerRoot.FindName("VerticalScrollBar")); if (vertical != null) vertical.ValueChanged += verticalHandler; }
Since the ScrollBar exposes a Maximum property, it is easy to attach to its ValueChanged event and fire an event requesting for an extra page if the new value and the maximum value match.
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.
void ScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { ScrollBar bar = sender as ScrollBar; if (e.NewValue == bar.Maximum && e.NewValue != lastValue) { // Fire an event requesting an extra page here! } lastValue = e.NewValue; }
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.
Binding Lights
For almost any developer who has worked in GUIs the concept of data binding should be at least familiar. If you have an Article and want to show its code and price, you just place two textboxes bound to those properties and place some invisible magical control who ensures that the controls and the data objects are kept synchronized. As long as you didn’t push it too far, it worked just fine, and allowed you to forget about some tedious GUI programming.
Silverlight, of course, has its own flavor of data binding, partially inherited from its big brother. This time, however, things have been made much easier.
Any Silverlight page or control has a visual tree, defined in XAML, which specifies a root container, its controls, their controls, and so on. Any Silverlight control has, as well, a property of type Object named DataContext. This property, when set to a control, is inherited by all of its children. Therefore, if you have a control C inside B inside A, and you set:
A.DataContext = myArticle;
C.DataContext = myName;
Then A and B will share myArticle as their DataContext, and C will have myName since it was manually set. To make C inherit its parent DataContext later, just set its property to null. Note that if you want to clear an object’s DataContext, then you can’t set it to null, you should set it to an empty object that has all the properties that the control expects.
What are these properties expected by the control? Silverlight has a special syntax to specify that a control’s property binds to one of it’s DataContext’s properties. Nothing better to begin with than the classic TextBox example:
<TextBlock Text="Name" />
<TextBlock Text="{Binding Name}" />
The first textblock will always display the text “Name”, whereas the second one will search for a property named “Name” in its DataContext and display its value. You can even set it to bind two-ways, as an easy way to allow user input:
<TextBlock Text="{Binding Name, Mode=TwoWay}" />
Note that if the property does not exist then the binding will fail silently (warning you by a small notice in the output console if you happen to be debugging). Nevertheless, Beta 2 introduced an event to which you can attach and get these kind of errors.
Because it fails silently, it is not mandatory that a control’s DataContext always provides all of the binded properties, so an easy and dirty way of clearing a data context is setting it to String.Empty.
Silverlight 2 in fact allows some interesting property paths, that allows you to bind to a property of a property of a property … of the data bound object, traverse through a collection’s hierarchy or refer to attached properties.
When you look for how to ensure that the GUI stays tuned with the data bound object’s properties whenever they change, you find an old interface from 2.0 filling the gap: INotifyPropertyChanged. If you are wondering if its cousin, INotifyCollectionChanged, has a role to play here, just consider the following scenario:
<TextBlock Text="{Binding CourseName}" />
<ListBox ItemsSource="{Binding Students}"/>
Consider this class model, with the class Course set as the visual tree’s datacontext:
class Course
{
public ObservableCollection
public String CourseName { get; set; }
}
class Student
{
public String Name { get; set; }
public int RegistrationNumber { get; set; }
public int CourseYear { get; set; }
public override string ToString() { return Name; }
}
What you will get is a TextBox displaying the course name, and a ListBox with the list of students in the course. Since the type of the Students collection is ObservableCollection, the most common INotifyCollectionChanged implementor, whenever you add a student to the course, the listbox is automatically updated.
If you want the same thing to happen with the name of the course, then you will have to manually implement INotifyPropertyChanged.
Note that you have a more complex property path for your bindings, suppose {Binding Course.Teacher.Address.Street}, then all objects in the path must implement INotifyPropertyChanged for it to refresh properly.
Going back to the example, you will notice that what is displayed as items in the ListBox are the ToString executions of all the Students contained in the items source collection. Though it is the easiest representation, it will hardly ever be the representation you want to show to the end user.
To solve this issue, Silverlight has included the concept of DataTemplates, which allow you to customize the way an object is rendered when contained in a presenter (such as a ListBoxItem).
But that is more than enough to fill a new post, which will be coming real soon.