Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / WPF
Article

Custom ListBox Layout in WPF

Rate me:
Please Sign up or sign in to vote.
4.98/5 (59 votes)
26 Apr 2007CPOL5 min read 400.3K   11.9K   108   40
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.

XML
<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.

C#
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:

XML
<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:

XML
<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:

XML
<!-- 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:

XML
<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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Josh creates software, for iOS and Windows.

He works at Black Pixel as a Senior Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

See his website Josh Smith Digital[^].

Comments and Discussions

 
QuestionThank ! Pin
Member 1202280326-Oct-15 11:01
Member 1202280326-Oct-15 11:01 
QuestionWhat to do with multiple listboxes Pin
Fumetsuj08-Feb-15 5:29
professionalFumetsuj08-Feb-15 5:29 
QuestionNot all images getting shown Pin
Member 941564618-Jun-14 4:31
Member 941564618-Jun-14 4:31 
QuestionHow to go through the whole list? ( using Right arrow key) Pin
Member 959381112-Jun-14 4:22
Member 959381112-Jun-14 4:22 
GeneralMy vote of 5 Pin
Bruno Costa Bourbon6-Jun-14 3:52
Bruno Costa Bourbon6-Jun-14 3:52 
QuestionVirtualize Pin
Dunge14-Mar-14 18:51
Dunge14-Mar-14 18:51 
Questionidea Pin
Pinigonzalez2-Jul-13 0:24
Pinigonzalez2-Jul-13 0:24 
GeneralMy vote of 5 Pin
Member 78556819-Feb-13 15:50
Member 78556819-Feb-13 15:50 
GeneralMy vote of 5 Pin
Theingi Win21-Aug-12 15:14
Theingi Win21-Aug-12 15:14 
GeneralGood sample Pin
Inba karthik2-Jul-12 19:05
Inba karthik2-Jul-12 19:05 
GeneralMy vote of 5 Pin
NeonMika10-Mar-12 6:16
NeonMika10-Mar-12 6:16 
GeneralMy vote of 5 Pin
Kaven Gagne18-Oct-11 17:07
Kaven Gagne18-Oct-11 17:07 
QuestionCoordinates of images. Pin
saisunil30-Sep-11 8:01
saisunil30-Sep-11 8:01 
GeneralMy vote of 5 Pin
Wil Peck10-Jun-11 7:18
Wil Peck10-Jun-11 7:18 
GeneralSuperb Article Pin
phil24157-Jun-11 0:12
phil24157-Jun-11 0:12 
Generalthanks Pin
cuipengfei20-Feb-11 17:52
cuipengfei20-Feb-11 17:52 
GeneralMy vote of 5! Pin
Tarun.K.S27-Oct-10 1:42
Tarun.K.S27-Oct-10 1:42 
GeneralMy vote of 5 Pin
la015-Oct-10 15:01
la015-Oct-10 15:01 
GeneralRandom rotated items. Problem with selected Image. [modified] Pin
Nightstacker16-Aug-10 7:32
Nightstacker16-Aug-10 7:32 
GeneralThank you very much Pin
Member 149481128-Jul-10 0:06
Member 149481128-Jul-10 0:06 
GeneralA Question regarding Non Symmetric Layout [modified] Pin
RunUFool19-Jul-10 23:04
RunUFool19-Jul-10 23:04 
Generalthank you Pin
giandeibrughi11-May-10 0:32
giandeibrughi11-May-10 0:32 
Questionany idea how to use both vertical and horizontal scroll for very large number of images above? Pin
funny_hacks8-Apr-10 2:52
funny_hacks8-Apr-10 2:52 
QuestionHow would you make the items wrap vertically Pin
bbehm1-May-09 7:29
professionalbbehm1-May-09 7:29 
AnswerRe: How would you make the items wrap vertically Pin
Josh Smith1-May-09 8:17
Josh Smith1-May-09 8:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.