#986 – Filtering a ListBox Using a CollectionViewSource

When populating a ListBox from a CollectionViewSource, you can also filter the data.  Below is an example of a ListBox that is bound to a CollectionViewSource.

    <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" Width="200" Height="200"
                 ItemsSource="{Binding Source={StaticResource cvsActors}}"/>
        <CheckBox Content="Only Joans" IsChecked="{Binding OnlyJoans}"
                  Margin="15"/>
    </StackPanel>

In code, we add a handler for the Filter event of the CollectionViewSource.  The handler is called for each item in the list.

            // Requires: using System.Windows.Data
            ((CollectionViewSource)this.Resources["cvsActors"]).Filter += ActorList_Filter;

In the handler, we set the Accepted property of the argument if the item should be included in the list.

        void ActorList_Filter(object sender, FilterEventArgs e)
        {
            // Set e.Accepted to true to include the item in the list
            if (!onlyJoans)
                e.Accepted = true;
            else
            {
                Actor a = e.Item as Actor;
                e.Accepted = (a.FirstName == "Joan") ? true : false;
            }
        }

986-001
986-002

We also have to make sure to “refresh” the CollectionViewSource when the OnlyJoans property changes.  This will trigger it to re-filter the collection.

        private bool onlyJoans;
        public bool OnlyJoans
        {
            get { return onlyJoans; }
            set
            {
                if (value != onlyJoans)
                {
                    onlyJoans = value;
                    RaisePropertyChanged("OnlyJoans");
                    ((CollectionViewSource)this.Resources["cvsActors"]).View.Refresh();
                }
            }
        }

#985 – Displaying Expandable Groups within a ListBox

You can group items in a ListBox using a CollectionViewSource.  You can then set the GroupStyle property of the ListBox to be an Expander control so that the groups can be expanded and collapsed.

In the example below, we group a collection of Actors by the decade of their birth.

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

    <StackPanel>
        <ListBox Name="lbActors" Margin="15" Width="200" Height="240"
                 ItemsSource="{Binding Source={StaticResource cvsActors}}">
            <ListBox.GroupStyle>
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style TargetType="{x:Type GroupItem}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate>
                                        <Expander Header="{Binding Name}" IsExpanded="True">
                                            <ItemsPresenter />
                                        </Expander>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
            </ListBox.GroupStyle>
        </ListBox>
    </StackPanel>

We can now expand and collapse the groups representing an actor’s birth decade.

985-001

 

985-002

#984 – Grouping Items in a ListBox with a CollectionViewSource

Similar to how you can use a CollectionViewSource to sort a collection of items in a ListBox, you can also use the CollectionViewSource to group the items, based on a property on the underlying object that you’re binding to.

Assume that you have an Actor object that has a LastName property and a DecadeBorn property that indicates what decade an actor was born in.  You can group the actors by decade and then sort within each decade by last name as shown below.

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

    <StackPanel>
        <ListBox Name="lbActors" Margin="15" Width="200" Height="240"
                 ItemsSource="{Binding Source={StaticResource cvsActors}}">
            <ListBox.GroupStyle>
                <GroupStyle/>
            </ListBox.GroupStyle>
        </ListBox>
    </StackPanel>

The empty GroupStyle element will cause the group to be rendered using a string representation of the DecadeBorn property.

We sort by decade and by last name within each decade.

984-001

984-002

#983 – Using a CollectionViewSource to Sort Items in a ListBox

You can sort items within a ListBox using a CollectionViewSource, which is a wrapper around a view of a collection.  The CollectionViewSource provides support for sorting, filtering and grouping items in the underlying collection.  It provides a mechanism for configuring the view from XAML.

In the example below, we define a CollectionViewSource that wraps a collection of Actor objects and specifies a property to sort on (the actor’s last name).  Our ListBox then binds to the CollectionViewSource rather than to the collection.

    <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" Width="200" Height="190"
                 ItemsSource="{Binding Source={StaticResource cvsActors}}"/>
    </StackPanel>

983-001
The Actor objects in our ListBox are now sorted by their LastName. (LastName is a property of the Actor object).
983-002

#982 – Executing Code When Selected Items in a ListBox Change

You can handle the SelectionChanged event for a ListBox to react to a change in the currently selected items.

Within the event handler, you can inspect the following properties of the SelectionChangedEventArgs parameter:

  • AddedItems – Collection of items just selected, type matches type of object in collection that ListBox is bound to
  • AddedInfos – Can get index and ListBoxItem for added items
  • RemovedItems – Collection of items just unselected
  • RemovedInfos – Get index and ListBoxItem for removed items
        <ListBox Name="lbActors" Margin="15" Width="200" Height="190"
                 ItemsSource="{Binding ActorList}"
                 SelectionMode="Extended"
                 SelectionChanged="lbActors_SelectionChanged"/>
        <TextBlock Name="tbInfo"/>

 

        private void lbActors_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            StringBuilder sbInfo = new StringBuilder();

            sbInfo.AppendLine("Added:");
            foreach (object o in e.AddedItems)
            {
                Actor a = o as Actor;
                sbInfo.AppendLine(a.FullName);
            }

            sbInfo.AppendLine("Removed:");
            foreach (object o in e.RemovedItems)
            {
                Actor a = o as Actor;
                sbInfo.AppendLine(a.FullName);
            }

            tbInfo.Text = sbInfo.ToString();
        }

982-001
982-002
982-003

#981 – Including a CheckBox with Each Item in a ListBox

By default, a ListBox shows the selection state of each item in the list using a different brush.  If you want to make the selection of each item more obvious and not force the user to use Ctrl or Shift keys to multi-select items, you can render each item as a CheckBox.

Assume that we bind to a a list of Actor objects, where Actor has IsFav (boolean) and NameAndDates (string) properties, you can do the following:

        <ListBox Name="lbActors" Margin="15" Width="200" Height="190"
                 ItemsSource="{Binding ActorList}">
            <!-- Because CheckBox indicates selection, hide standard ListBox -->
            <!-- selection behavior -->
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Focusable" Value="False"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <!-- Each item in list is a CheckBox -->
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <CheckBox Content="{Binding NameAndDates}" IsChecked="{Binding IsFav}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

As you check each item, the IsFav property in the corresponding Actor object is set/cleared.

981-001

#980 – Binding ListBox Selection to Property on Bound Object

Normally, when you use a ListBox, the ListBox itself persists the record of which items are selected.  You might also want to persist information about which items are selected within the actual data objects that you’re binding to.

You can bind the IsSelected property of each list box item to a boolean property on the data object as shown below.

        <ListBox Name="lbActors" Margin="15" Width="200" Height="190"
                 SelectionMode="Multiple"
                 ItemsSource="{Binding ActorList}">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="IsSelected" Value="{Binding IsFav}"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>

Now, as you select/unselect items, the IsFav property in the corresponding Actor object is set or cleared.  Also, when your application starts, the ListBox selection will initially be set to reflect the value of this property.

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            StringBuilder sbInfo = new StringBuilder();

            // Who are favorites?
            foreach (Actor a in lbActors.Items)
            {
                if (a.IsFav)
                    sbInfo.AppendLine(a.FullName);
            }

            MessageBox.Show(sbInfo.ToString());
        }

980-001

#979 – Selecting Items in a ListBox Programmatically

The ListBoxItem object, representing a single item in a ListBox, contains an IsSelected property that you can read in order to determine whether the item is selected.  The IsSelected property can be read from to determine if the item is selected.  It can also be written to, to select the item.

The code below selects every other item in a ListBox.  Note that we need to call the ItemContainerGenerator.ContainerFromIndex helper method in order to get the ListBoxItem for each item.

private void btnWrite_Click(object sender, RoutedEventArgs e)
{
    // Select every other item, starting with
    // the first.
    int i = 0;
    while (i < lbActors.Items.Count)
    {
        // Get item's ListBoxItem
        ListBoxItem lbi = (ListBoxItem)lbActors.ItemContainerGenerator.ContainerFromIndex(i);
        lbi.IsSelected = true;
        i += 2;
    }
}

979-001

#978 – Discovering Whether an Item in a ListBox Is Selected

The ListBoxItem object, representing a single item in a ListBox, contains an IsSelected property that you can read in order to determine whether the item is selected.

If you use data binding to populate the list, the objects in the ListBox’s Items property will not be ListBoxItem instances, but will have a type matching the bound object.

You can use the ContainerFromItem helper method to get the corresponding ListBoxItem so that you can then query it to determine if the object is selected.

        private void btnRead_Click(object sender, RoutedEventArgs e)
        {
            StringBuilder sbInfo = new StringBuilder();

            // Each item is an Actor
            foreach (Actor a in lbActors.Items)
            {
                // Get item's ListBoxItem
                ListBoxItem lbi =
                  (ListBoxItem)lbActors.ItemContainerGenerator.ContainerFromItem(a);

                if (lbi.IsSelected)
                    sbInfo.Append("YES ");
                else
                    sbInfo.Append("no ");
            }

            MessageBox.Show(sbInfo.ToString());
        }

978-001

#977 – DisplayMemberPath Indicates Property to Use for Displaying Bound Items

When you use data binding to bind an ItemsControl to a collection, there are several ways to control how each item in the list is rendered:

  • Rely on the ToString method of the bound item to generate a string to be displayed
  • Use a data template to create a more complex rendering of the item
  • Use the DisplayMemberPath property to indicate which property should be used in generating a string

DisplayMemberPath can be set to the name of a property (of type string) on the bound object.  The value of that property is then used as the displayed value of each item in the list.

Below, the KnownFor property of an Actor is used to generate the string in the ListBox.

        <ListBox Margin="15" Width="200" Height="150"
                 ItemsSource="{Binding ActorList}"
                 DisplayMemberPath="KnownFor"
                 SelectedItem="{Binding SelectedActor}"/>
        <TextBlock Text="{Binding SelectedActor.FullName}"
                   HorizontalAlignment="Center" Margin="0,5"/>

977-001