#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

#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

#989 – Enabling Live Filtering in a CollectionViewSource

Like sorting, filtering in a CollectionViewSource is not automatically done when you change the contents of one of the data bound items.  By default, you need to call the Refresh method of the CollectionViewSource.

In the example below, we filter on first name “Joan” and then change Joan Fontaine’s first name to “Bob”.  Notice that the list is not re-filtered–Bob remains in the list.

989-001

989-002

You can fix this by adding the FirstName property to the LiveFilteringProperties collection of the CollectionViewSource and setting IsLiveFilteringRequested to true.

        <CollectionViewSource x:Key="cvsActors" Source="{Binding ActorList}"
                              IsLiveFilteringRequested="True">
            <CollectionViewSource.LiveFilteringProperties>
                <clr:String>FirstName</clr:String>
            </CollectionViewSource.LiveFilteringProperties>
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="LastName" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>

Now, when we change Joan to Bob and we’re filtering on “Joan”, Bob automatically disappears from the list.

989-003

#988 – Enabling Live Sorting in a CollectionViewSource

By default, when you’re using a CollectionViewSource to do sorting, grouping and filtering in a list-based control, the sorting/grouping/filtering behavior will only updated when you explicitly refresh the CollectionViewSource (by calling Refresh) or when you add or remove something to the collection.

You can enable live sorting in the CollectionViewSource to cause it to resort items when one or more properties on the bound objects change.  In the example below, we set the IsLiveSortingRequested property to true and specify that the Actor.LastName property is the property to live sort on.

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

Now when we make a change to the last name of one of the actors, the sorting is updated.

988-001

 

988-002

#987 – CollectionViewSource Updates on Refresh or Change to Collection

By default, when you’re using a CollectionViewSource to do sorting, grouping and filtering in a list-based control, the sorting/grouping/filtering behavior will only updated when you explicitly refresh the CollectionViewSource (by calling Refresh) or when you add or remove something to the collection.

For example, if we add an actor to a list of actors and we are sorting by last name, the actor will be inserted at the correct spot.

987-001

 

987-002

 

If you change the contents of one of the bound objects, however, the sorting behavior is not updated.  For example, if we change the last name of “Jennifer Jones” to “Garner” and we do not call the Refresh method of the CollectionViewSource, the sorting will be wrong.

987-003