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

SelectedItems Behavior for ListBox and MultiSelector

0.00/5 (No votes)
26 Sep 2017 3  
A behavior that allows the developer to bind an IList to SelectedItems of a ListBox or MultiSelector.

Introduction

I wanted to do a straight forward implementation of a ViewModel with ItemsSource and SelectedItems. I could have just wrapped the ItemsSource ViewModel in an Adapter with an IsCheckedProperty, but it seemed simpler to just have a SelectedItems property. However, the ListBox SelectedItems property is not a DependencyProperty.

Design

I created a behavior with a SelectedItems property:

public class SelectedItemsBahavior
{
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached("SelectedItems",

         typeof(INotifyCollectionChanged), typeof(SelectedItemsBahavior),
        new PropertyMetadata(default(IList), OnSelectedItemsChanged));

    public static void SetSelectedItems(DependencyObject d, INotifyCollectionChanged value)
    {
        d.SetValue(SelectedItemsProperty, value);
    }

    public static IList GetSelectedItems(DependencyObject element)
    {
        return (IList)element.GetValue(SelectedItemsProperty);
    }

    private static void OnSelectedItemsChanged(DependencyObject d,
                      DependencyPropertyChangedEventArgs e)
    {
        IList selectedItems = null;
         void CollectionChangedEventHandler(object sender, NotifyCollectionChangedEventArgs args)
        {
             if (args.OldItems != null)
                foreach (var item in args.OldItems)
                    if (selectedItems.Contains(item))
                        selectedItems.Remove(item);

            if (args.NewItems != null)
                foreach (var item in args.NewItems)
                    if (!selectedItems.Contains(item))
                        selectedItems.Add(item);
        };

        if (d is MultiSelector multiSelector)
        {
            selectedItems = multiSelector.SelectedItems;
            multiSelector.SelectionChanged += OnSelectionChanged;
        }
        if (d is ListBox listBox)
        {
            selectedItems = listBox.SelectedItems;
            listBox.SelectionMode = SelectionMode.Multiple;
            listBox.SelectionChanged += OnSelectionChanged;
        }
        if (selectedItems == null) return;

        if (e.OldValue is INotifyCollectionChanged)
            (e.OldValue as INotifyCollectionChanged).CollectionChanged
                           -=  collectionChangedEventHandler ;
        if (e.NewValue is INotifyCollectionChanged)
            (e.NewValue as INotifyCollectionChanged).CollectionChanged
                           +=  collectionChangedEventHandler ;
    }

    private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var s = sender as DependencyObject;
        if (!GetIsBusy(s))
        {
            SetIsBusy(s, true);
            var list = GetSelectedItems((DependencyObject)sender);
            foreach (var item in e.RemovedItems)
                if (list.Contains(item)) list.Remove(item);
            foreach (var item in e.AddedItems)
                if (!list.Contains(item)) list.Add(item);
            SetIsBusy(s, false);
        }
    }

    private static readonly DependencyProperty IsBusyProperty =
        DependencyProperty.RegisterAttached("IsBusy", typeof(bool),

              typeof(SelectedItemsBahavior), new PropertyMetadata(default(bool)));

    private static void SetIsBusy(DependencyObject element, bool value)
    {
        element.SetValue(IsBusyProperty, value);
    }

    private static bool GetIsBusy(DependencyObject element)
    {
        return (bool)element.GetValue(IsBusyProperty);
    }
}

The design will support both a ListBox and a MultiSelector. The only difference is that class that I Cast to since both have the SelectedItems property of type IList which means both have the event SelectionChanged event.

I have to handle changes in the control's SelectedItems property and the IList bound to the behavior's SelectedItems DependencyProperty to keep the two synchronized. That means handling the collection changed events for both collection.

The control's SelectionChanged event is handled by the OnSelectionChanged event handler which will synchronize any added or removed ItemsSource items with the SelectedItems DependencyProperty based on the SelectionChangedEvenArgs properties.

There is also a NotifyCollectionChangedEventHandler that is defined in the DependencyProperty changed event handler. Note that a normal List does not have a CollectionChanged event, so if the ViewModel needs to make changes to the SelectedItems, then property that is bound to the SelectedItems property must be of a Type that is derived from the interface INotifyCollectionChanged such as ObservableCollection.

If the property is of type INotifyCollectionChanged, then the CollectionChanged event will be handled by the CollectionChangedEventHandler, and this method will synchronize any changes with the control's SelectedItems property.

Using the Code

Using this behavior is really easy, all that needs to be done is to use the behavior to bind to the property that is to contain the SelectedItems collection in the ViewModel:

<ListBox ItemsSource="{Binding ItemsSource}" 

         local:SelectedItemsBahavior.SelectedItems="{Binding SelectedItems}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <CheckBox Name="CheckBoxZone"

                  IsChecked="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
                                          Path=IsSelected}">
                <TextBlock Width="{Binding ElementName=SizingBorder,
                                           Path=ActualWidth}"

                           Foreground="Black"

                           Text="{Binding}"

                           TextWrapping="Wrap" />
            </CheckBox>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

A Bonus

It should be noticed that I used a DataTemplate for the ListBox defined above, and bound its IsChecked property to the ListBoxItem IsSelected property. This is a non invasive way to create a CheckBox List in a ListBox.

History

  • 09/26/2017: Initial version

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