Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom ListBox Layout in WPF

0.00/5 (No votes)
26 Apr 2007 6  
A step-by-step review of how to customize the arrangement of items in a ListBox.

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"
 >
  <!-- 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. -->
  <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:

Screenshot - CustomItemsPanel_NoTemplate.png

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}">
      <!-- Set the ItemTemplate of the ListBox to a DataTemplate which
           explains how to display an object of type BitmapImage. -->
      <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>

  <!-- 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. -->
  <ListBox ItemsSource="{Binding}" />
</Window>

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

Screenshot - CustomItemsPanel_WithTemplate.png

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:

<!-- Swap out the default items panel with a WrapPanel so that
     the images will be arranged with a different layout. -->
<Setter Property="ItemsPanel">
  <Setter.Value>
    <ItemsPanelTemplate>
      <WrapPanel />
    </ItemsPanelTemplate>
  </Setter.Value>
</Setter>

<!-- 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. -->
<Setter
  Property="ScrollViewer.HorizontalScrollBarVisibility"
  Value="Disabled"
  />

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

Screenshot - CustomItemsPanel_WrapPanel.png

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

Screenshot - CustomItemsPanel_WrapPanel_Wide.png

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here