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
.
<Window x: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"
>
-->
<ListBox ItemsSource="{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.
public static class RobotImageLoader
{
public static List<BitmapImage> LoadImages()
{
List<BitmapImage> robotImages = new List<BitmapImage>();
DirectoryInfo robotImageDir = new DirectoryInfo( @"..\..\Robots" );
foreach( FileInfo robotImageFile in robotImageDir.GetFiles( "*.jpg" ) )
{
Uri uri = new Uri( robotImageFile.FullName );
robotImages.Add( new BitmapImage( uri ) );
}
return robotImages;
}
}
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:
<Window.DataContext>
<ObjectDataProvider
ObjectType="{x:Type local:RobotImageLoader}"
MethodName="LoadImages"
/>
</Window.DataContext>
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:
<Window x: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>
<Style TargetType="{x:Type ListBox}">
-->
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="4"
CornerRadius="5" Margin="6"
>
<Image
Source="{Binding Path=UriSource}"
Stretch="Fill"
Width="100" Height="120"
/>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Window.DataContext>
<ObjectDataProvider
ObjectType="{x:Type local:RobotImageLoader}"
MethodName="LoadImages" />
</Window.DataContext>
-->
<ListBox ItemsSource="{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:
<!---->
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<!---->
<Setter
Property="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:
<Setter
Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled"
/>
The source code for this demo project can be downloaded at the top of this article.
History
- April 25, 2007 � Created article