#999 – Using a Canvas as the Items Panel for a ListBox

You can replace the default StackPanel used as the items panel for a ListBox with any other panel element.  If you have items that you want to display at arbitrary locations, you can use a Canvas for your items panel.

The example below presents a list of cities, where each city is placed at its proper latitude and longitude.

Assuming that we have a City class that accepts a name and latitude/longitude values passed to its constructor, we can create a list of cities:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;

            CityList = new ObservableCollection<City>
            {
                new City("Duluth", 46.83, 92.18),
                new City("Redmond", 44.27, 121.15),
                new City("Tucson", 32.12, 110.93),
                new City("Denver", 39.75, 104.87),
                new City("Boston", 42.37, 71.03),
                new City("Tampa", 27.97, 82.53)
            };
        }

        private ObservableCollection<City> cityList;
        public ObservableCollection<City> CityList
        {
            get { return cityList; }
            set
            {
                cityList = value;
                RaisePropertyChanged("CityList");
            }
        }

        // INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        private void RaisePropertyChanged(string propName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

We then bind a ListBox to this list of cities.  We also:

  • Use its ItemContainerStyle to map latitude and longitude values to the attached Top and Left properties of the Canvas element
  • Use value converters to convert latitude and longitude values to canvas positions
  • Specify the Canvas as the ItemsPanel
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow"
        Width="470" Height="310">

    <Window.Resources>
        <ResourceDictionary>
            <local:LatValueConverter x:Key="latValueConverter" />
            <local:LongValueConverter x:Key="longValueConverter" />
            <sys:Double x:Key="mapWidth">440</sys:Double>
            <sys:Double x:Key="mapHeight">240</sys:Double>
        </ResourceDictionary>
    </Window.Resources>

    <StackPanel Orientation="Horizontal" Margin="5" >
        <ListBox ItemsSource="{Binding CityList}"
                 DisplayMemberPath="Name">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Canvas.Left"
                            Value="{Binding Longitude, Converter={StaticResource longValueConverter},
                                            ConverterParameter={StaticResource mapWidth}}"/>
                    <Setter Property="Canvas.Top"
                            Value="{Binding Latitude, Converter={StaticResource latValueConverter},
                                            ConverterParameter={StaticResource mapHeight}}"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas IsItemsHost="True"
                            Width="{StaticResource mapWidth}"
                            Height="{StaticResource mapHeight}"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </StackPanel>
</Window>

Here is the implementation of the value converters:

    public static class Constants
    {
        public const double LatTop = 50.0;
        public const double LatBottom = 24.0;

        public const double LongLeft = 125.0;
        public const double LongRight = 66.0;
    }

    public class LatValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double latitude = (double)value;
            double height = (double)parameter;

            int top = (int)((Constants.LatTop - latitude) / (Constants.LatTop - Constants.LatBottom) * height);
            return top;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public class LongValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double longitude = (double)value;
            double width = (double)parameter;

            int left = (int)((Constants.LongLeft - longitude) / (Constants.LongLeft - Constants.LongRight) * width);
            return left;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

The end result is that the city names are displayed at their proper locations on the canvas.  Note that because the cities are displayed in a ListBox, we can still select one of them.

999-001

Advertisement

#998 – Orient a ListBox Horizontally

You can make a ListBox render its items horizontally, rather than vertically, by setting its ItemsPanel.

In the example below, we set the ItemsPanel to a template containing a horizontally-oriented StackPanel.  (The default is a vertically-oriented StackPanel).

        <ListBox Width="230" Height="70"
                 ItemsSource="{Binding ActorList}"
                 DisplayMemberPath="LastName">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel IsItemsHost="True" Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>

998-001

#997 – Possible Problems with ItemContainerGenerator

There are cases when you might use the ItemContainerGenerator type to get the corresonding ListBoxItem for a particular item in a ListBox.  For example, the code below will select every other item in a ListBox.

            // Select every other item, starting with
            // the first.
            int i = 0;
            while (i < lbNumbers.Items.Count)
            {
                // Get item's ListBoxItem
                ListBoxItem lbi = (ListBoxItem)lbNumbers.ItemContainerGenerator.ContainerFromIndex(i);
                lbi.IsSelected = true;
                i += 2;
            }

The problem with this code is that if UI virtualization is enabled for the ListBox, the ItemContainerGenerator methods will return null for items that are not currently visible.  This happens because the corresponding ListBoxItem has not yet been created.

To avoid this, you can either find an alternative method to using ItemContainerGenerator, or you can scroll the corresponding item into view before calling the method to get its container.

#996 – Turning off UI Virtualization in a ListBox

By default, a ListBox uses UI virtualization, creating UIElements for each list item only as they are scrolled into view.  This is normally what you want, since using UI virtualization improves the performance of the ListBox when it contains a large number of items.

You can, however, disable UI virtualization by setting the VirtualizingPanel.IsVirtualizing property on the ListBox to false.

In the example below, we load two ListBox controls with a list of 100 numbers.  We turn off UI virtualization for the second ListBox and then examine the visual tree of each ListBox.

        <ListBox Name="lbDefault" Margin="15,10" Width="70" Height="200"
                 ItemsSource="{Binding NumberList}" />

        <ListBox Name="lbNoVirtualization" Margin="15,10" Width="70" Height="200"
                 VirtualizingPanel.IsVirtualizing="False"
                 ItemsSource="{Binding NumberList}" />

In the ListBox that does use UI virtualization, the visual tree shows that it contains only a small number of ListBoxItems.
996-001
In the second ListBox, where we turned off UI virtualization, the visual tree contains a ListBoxItem for each of the 100 items in the source data.

996-002

#995 – ListBox Uses UI Virtualization by Default

List-based controls in WPF are comprised of a panel that contains a child UIElement for each item in the list.  The visual tree for a ListBox includes a VirtualizingStackPanel as a container for a series of ListBoxItem instances.  The ListBoxItem is the user interface element that renders an item from the list.

994-002

When a list contains a large number of items, it would take a long time and a large amount of memory to create a ListBoxItem for each element in the list.

To improve performance, a ListBox uses UI virtualization by default.  UIElement-based objects are created only for the items currently being displayed in the list.  New UIElements are then created as additional items are scrolled into view.

We can see this by binding a ListBox to a collection containing 1,000 elements and then looking at its visual tree.  ListBoxItems have been created for only the first few items.

995-001

#994 – Viewing the Visual Tree from within Visual Studio

A visual tree in WPF is the complete hierarchy of all visual elements that make up your user interface.  The visual tree will contain lower-level elements that are not necessarily part of the higher-level logical tree, as defined in your XAML.

You can view the visual tree for UI elements in an WPF application from within Visual Studio, as follows.

Add a breakpoint in the code for your main window that occurs after the application has loaded.  In the example below, we break after pressing a Button.

In the Locals window, find the object representing your main window.  In the Value column, hover over the magnifying glass and notice that it’s labeled WPF Tree Visualizer.

994-001

 

Click on the magnifying glass to open the WPF Tree Visualizer.  The WPF Tree Visualizer will open in a new window.  You can view the Visual Tree in the upper left corner of the window.

994-002

#993 – Default Control Template for a ListBox

The default control template used for a ListBox (in version 4.5 of the .NET Framework) is shown below.  The template is included within a style that includes some other default property values.

The core structure of the ListBox is simple–an ItemsPresenter, within a ScrollViewer and surrounded by a Border.

    <Window.Resources>
        <Style x:Key="lbDefaultStyle" TargetType="{x:Type ListBox}">
            <Setter Property="Background" Value="White"/>
            <Setter Property="BorderBrush" Value="#FFABADB3"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
            <Setter Property="ScrollViewer.PanningMode" Value="Both"/>
            <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBox}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="True">
                            <ScrollViewer Focusable="False" Padding="{TemplateBinding Padding}">
                                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </ScrollViewer>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="Background" TargetName="Bd" Value="White"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="#FFD9D9D9"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsGrouping" Value="True"/>
                                    <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="False"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="ScrollViewer.CanContentScroll" Value="False"/>
                            </MultiTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <StackPanel>
        <ListBox Name="lbActors" Margin="15,5" Width="200" Height="200"
                 ItemsSource="{Binding ActorList}"
                 Style="{DynamicResource lbDefaultStyle}"/>
    </StackPanel>

#992 – Scrolling an Item in a ListBox into View

You can programmatically scroll an item in a ListBox into view by using the ListBox.ScrollIntoView method.

In the example below, we scroll the actor Jane Wyman into view when the user clicks the button.

        <ListBox Name="lbActors" Margin="15,5" Width="200" Height="150"
                 ItemsSource="{Binding ActorList}"
                 DisplayMemberPath="NameAndDates"/>
        <Button Content="Find Jane Wyman" Margin="10"
                Click="btnFindJane_Click"/>

992-001

In the Click event handler for the button, we use Linq to find the Actor object for Jane Wyman and we then call ScrollIntoView.

        private void btnFindJane_Click(object sender, RoutedEventArgs e)
        {
            Actor jane = (from a in ActorList
                         where a.FirstName == "Jane"
                            && a.LastName == "Wyman"
                         select a).First();
            lbActors.ScrollIntoView(jane);
            lbActors.SelectedItem = jane;
        }

992-002

#991 – Specifying which Field Is Used for Finding an Item by Typing

You can normally jump to a particular item in an items control by typing the first few characters of the item.  This works, however, based on the string representation of each bound item (the output of ToString).  In some cases, you want to use a specific field when finding an item by typing text.  You can do this by setting TextSearch.TextPath on the list control.

In the example below, we have a data template that displays last and first names of each actor.  We specify that we want to use the last name as a search path.

        <ListBox Name="lbActors" Margin="15,5" Width="200" Height="220"
                 ItemsSource="{Binding ActorList}"
                 TextSearch.TextPath="LastName">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding LastCommaFirst}"/>
                        <TextBlock Text="{Binding Dates}"
                                   FontStyle="Italic"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

We can now just start typing an actor’s last name in order to jump to that actor.  (E.g. “Crawford”).

991-001

#990 – Typing Text to Select an Item in a ListBox

If a ListBox has focus, you can just type some text in order to select an item.  By default, the text that you enter will be matched against the property specified by the DisplayMemberPath property, or by the value of the bound object’s ToString method, if DisplayMemberPath is not specified.

In the example below, the NameAndDates property is used as the display string.  A CollectionViewSource is used to sort by last name.

    <Window.Resources>
        <CollectionViewSource x:Key="cvsActors" Source="{Binding ActorList}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="LastName" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Window.Resources>

    <StackPanel>
        <ListBox Name="lbActors" Margin="15,5" Width="200" Height="190"
                 ItemsSource="{Binding Source={StaticResource cvsActors}}"
                 DisplayMemberPath="NameAndDates" />
    </StackPanel>

Once the ListBox has focus, we can type a letter to jump to the next item starting with that letter.  For example, if we enter ‘J’ and then enter ‘J’ again, Joan Crawford is first selected, followed by Joan Fontaine.

990-001

990-002