Introduction

This article explores how to customize the way that items are arranged in a ListBox (or any ItemsControl subclass). It makes use of the ItemsPanel property to perform the customization.

Background

I had an "Aha!" moment one day when I discovered the ItemsPanel property of ItemsControl. This property allows you to choose the layout panel used to arrange items displayed in an ItemsControl or any control which derives from it, such as ListBox. This feature is evidence of the incredible flexibility in WPF because it allows you to completely redefine how the items in a list should be arranged, relative to one another.

The demo application shown here populates a ListBox with images of toy robots. Initially the images are listed from the top of the ListBox down to the bottom, which is the normal behavior. After the customization is complete, the images will be displayed in a left-to-right top-to-bottom layout, like text on a page (for us left-to-right readers). This custom layout is achieved by using a WrapPanel to arrange the images for us.

Step one – Putting a ListBox in a Window

The implementation of this task can be broken into four logical steps. The first step is just to put a ListBox into a Window.

<Windowx:Class="CustomItemsPanel.Window1"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:CustomItemsPanel"Title="Custom ItemsPanel"Height="600"Width="260"><!--<span class="code-comment"> This ListBox is the Content of the Window.
Normally you would have a panel of some type
as the Window's Content, but let's keep it simple. --></span><ListBoxItemsSource="{Binding}"/></Window>

If you compile and run the project at this point, you will see what appears to be an empty Window. The ListBox is displayed there; however, it has no items in it yet.

Step two – Filling the ListBox with pictures

Next let's see the class which is used to populate the ListBox with images of robots.

The simple code above assumes that your project has a folder named "Robots" and it contains some JPG images. In a more realistic application, these types of hard-coded dependencies should be externalized into a configuration system. We can make use of the RobotImageLoader class with the following markup in the Window class declared above:

The XAML above indicates that the implicit data source for all visual elements in the Window will, by default, be the object returned when calling the static RobotImageLoader.LoadImages method.

If you run the application now and resize the Window a bit, it looks like this:

The screenshot seen above is obviously not what we had in mind. It would be much nicer if we could see the image stored within a BitmapImage, instead of the image's URI. The reason it is displaying a URI is because a BitmapImage object has no intrinsic support for displaying itself. When the ListBox renders each BitmapImage object, it ends up calling the ToString method on the object because BitmapImage does not derive from the UIElement class. It then displays the string returned from the BitmapImage object's ToString override.

Step three – Creating a template to display pictures

The next step is to explain to the ListBox how it should render a BitmapImage. To accomplish this, we will apply a Style to the ListBox. The Style will set the ListBox's ItemTemplate property to a DataTemplate, which specifies that an Image element wrapped in a Border should be displayed when trying to render a BitmapImage object.

Here's the modified XAML:

<Windowx:Class="CustomItemsPanel.Window1"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:CustomItemsPanel"Title="The images are shown"Height="600"Width="260"><Window.Resources><StyleTargetType="{x:Type ListBox}">
<!--<span class="code-comment"> Set the ItemTemplate of the ListBox to a DataTemplate which
explains how to display an object of type BitmapImage. --></span><SetterProperty="ItemTemplate"><Setter.Value><DataTemplate><BorderBorderBrush="Black"BorderThickness="4"CornerRadius="5"Margin="6"><ImageSource="{Binding Path=UriSource}"Stretch="Fill"Width="100"Height="120"/></Border></DataTemplate></Setter.Value></Setter></Style></Window.Resources><Window.DataContext><ObjectDataProviderObjectType="{x:Type local:RobotImageLoader}"MethodName="LoadImages"/></Window.DataContext><!--<span class="code-comment"> This ListBox is the Content of the Window.
Normally you would have a panel of some type
as the Window's Content, but let's keep it simple. --></span><ListBoxItemsSource="{Binding}"/></Window>

If you run the application now, it looks like this:

Ah, that's much better. Notice, though, that the user would have to scroll down if he/she wanted to see more of the robots. Perhaps the logic of this application requires that the user should be able to see as many robots as possible in the Window. This is when the ItemsPanel property saves the day.

Step four – Replacing the default items panel

By default the ListBox uses what's called a VirtualizingStackPanel to display its items. Basically, a VirtualizingStackPanel is a StackPanel that only creates visual objects for the items that are currently viewable in the control. For items that are scrolled out of view, the panel throws away the visual objects used to render them. This technique can drastically improve performance and memory consumption when the control has a large number of items.

For situations where a VirtualizingStackPanel is not the ideal layout mechanism for items in the ListBox, we can specify any panel we would like to display the items. A good choice for our situation here is to use the WrapPanel to host the ListBox's items. The WrapPanel, by default, will arrange its children from left to right and, when it runs out of horizontal space, it will create another row of items beneath the previous row. It keeps following that pattern until all of the items are displayed. When the WrapPanel is resized it will update the layout to ensure that as many of the items are entirely in view as possible.

The last step is to set the ListBox's ItemsPanel property to a WrapPanel. The following XAML would also be placed in the Style seen in the previous snippet:

<!--<span class="code-comment"> Swap out the default items panel with a WrapPanel so that
the images will be arranged with a different layout. --></span><SetterProperty="ItemsPanel"><Setter.Value><ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate></Setter.Value></Setter><!--<span class="code-comment"> Set this attached property to 'Disabled' so that the
ScrollViewer in the ListBox will never show a horizontal
scrollbar, and the WrapPanel it contains will be constrained
to the width of the ScrollViewer's viewable surface. --></span><SetterProperty="ScrollViewer.HorizontalScrollBarVisibility"Value="Disabled"/>

When you run the application now, it looks like this:

If you were to resize the Window, the WrapPanel would adjust the layout to accommodate the new dimensions. For example:

There is one important thing to notice in the XAML seen above. It is necessary to specify that the ScrollViewer inside the ListBox disables its horizontal scrollbar. Doing so ensures that the width of the WrapPanel is constrained to the viewable width of the ScrollViewer. It also prevents the horizontal scrollbar from ever appearing, which is desirable in this scenario. Here's the XAML in the <Style> which sets that property: