Multi level control

The Patient Journey Demonstrator application that I blogged about in an earlier post shows some nice new UI controls. One of the most useful new features is a “Level of Detail” slider. This slider is used in the demonstrator to specify the level of detail of the content in a listbox. Move the slider to the left and only the basic information is shown. Moving the slider to the right and more and more details are revealed. Descriptions are good, images better :)

Level of Detail demonstration

This window demonstrates three levels that can be selected with the slider. In this example three levels are defined and the selected item is always maximized (level 3). I have created a sample program to illustrate its use (download below).

So how does it work?
At first I tried a XAML only solution, but it requires a lot of styles, templates and you have to define them over and over again. Then I thought of a more elegant solution to use a custom control that is located in the DataTemplate. It has a Level property that determines which panel is displayed. Bind the Level property to the slider control and you’re done.

Because this kind of control is often used when making selections, I have added two additional dependency properties that can be used to display the selected item in a special way. Specify a template for the selected item in one of the levels and set the MaxLevel to this template. If you now set the UseMaxLevel property to true, it displays the template in MaxLevel instead of the Level value. An example of this behavior is shown in the sample application.

How do you use it?
Take a look at the sample code and I think it will become clear how to use it. If not, then perform the following.

First you need to define how you can select the level of detail requested. My sample uses a slider, but you can also select it using radio buttons, comboboxes, etc. The most important part is that the result of the selection is an integer value between 0 and 5. Suppose that you also want to use a slider, then add the following code to your XAML file:

<Slider Name="detailSlider" Width="50" Minimum="0" Maximum="4" Value="0"/>

Now you can use the <DetailLevel> element and bind it to the slider control, like this:

<local:DetailLevelWrapper 
    Level="{Binding ElementName=detailSlider, Path=Value}">
 
    <!-- Level 0 -->
    <local:DetailLevelWrapper.Level0Template>
        <ControlTemplate>
            <TextBlock Text="Level 0"/>
        </ControlTemplate>
    </local:DetailLevelWrapper.Level0Template>
 
    <!-- Level 1 -->
    <local:DetailLevelWrapper.Level1Template>
        <ControlTemplate>
            <TextBlock Text="Level 1"/>
        </ControlTemplate>
    </local:DetailLevelWrapper.Level1Template>
 
    <!-- Level 2 -->
    <local:DetailLevelWrapper.Level2Template>
        <ControlTemplate>
            <TextBlock Text="Level 2"/>
        </ControlTemplate>
    </local:DetailLevelWrapper.Level2Template>
 
</local:DetailLevelWrapper>

Make sure you have a reference to the namespace local at the top of your XAML file that points to the proper namespace (and assembly, if you decide to put it into a seperate assembly). The current implementation only supports a maximum of five levels.

If you run your program and move the slider, you will see that the text changes between Level1, Level2 and Level3. Not very exciting, but if you put it inside a DataTemplate and assign it to a listbox, then you can get more exciting results.

You don’t need to put the DetailLevelWrapper at the root of your DataTemplate. You can use it as often as you want inside the DataTemplate, so you can share common XAML parts between different levels.

[Update 21-2-2009]
I have updated the control, so it uses a ControlTemplate. This is much more efficient.

Sample application
DetailLevelWrapper demonstration program (with source code)

Virtual collections (part 1)

In the era of Win32/MFC the virtual listbox (and virtual listview) was a popular method to efficiently display large collections. In WPF there is no standard way to efficiently display a large collection of items. For each control that is derived from ItemsControl (such as the ListBox) there are two important items in the control:

  1. The UI element that is used to display the item.
  2. The actual .NET object that is represented by the UI element.

Most ItemsControl derived classes use a VirtualizingStackPanel object to display the items in the control. This object only creates the UI elements that are actually displayed on screen. If you have a Listbox containing 10.000 elements and only some are displayed on screen, then VirtualizingStackPanel only creates the UI elements for the visible items. This sounds great, but the underlying collection still contains 10.000 elements. If you need to collect these elements from another tier (i.e. via a webservice), then it takes a very long time to populate the listbox.

In the next paragraphs I will describe the concept of “virtual collections” to efficiently populate the collection. A virtual collection is a collection containing only stubs to the actual items. If an item is accessed, then it is created on demand. If only the first 10 elements in a 10.000 item collection are accessed, then only the first 10 elements are actually created. This can greatly improve performance for large collections that are expensive to populate.

ItemsControl derived class uses an ItemsSource property that accepts an object that implements the IEnumerable interface. It is much more efficient to supply an object that also implements the IList interface, because it supports indexed access to the underlying collection. I haven’t found any documentation, but I found out that if your collection support IList, then the IList interface is used in favor of the (less efficient) IEnumerable interface. The virtual collection that is presented here supports the IList interface (and implicitly support IEnumerable as well). That’s why it is compatible with all ItemsSource derived controls.

The concept is easy. During the creation of the virtual collection an array is allocated that only contains null pointers. When the indexing operator [] is used, then the underlying object is created and stored inside the array. The next time the same item is accessed, then it is already there and doesn’t need to be created anymore.

The virtual collection uses a generator to create the underlying objects. The object generator should implement the IObjectGenerator interface and must be provided by the programmer. This interface only has one property and one method:

  • The Count property returns the number of items in the collection.
  • The CreateObject method creates a new object based on the given index.

It is very easy to use the virtual collection. Implement an object that implements IObjectGenerator<T> and pass it to the VirtualList<T>‘s constructor. Refer to the attached example code for an example how to use and bind the collection.

In a next part I will show you how to add paging to IObjectGenerator to reduce the number of roundtrips. This is useful for collections that are large and the roundtrip to create the objects is expensive.