Click here to Skip to main content
15,615,945 members
Articles / Desktop Programming / WPF
Tip/Trick
Posted 26 Sep 2017

Tagged as

Stats

19.1K views
441 downloads
10 bookmarked

SelectedItems Behavior for ListBox and MultiSelector

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
26 Sep 2017CPOL2 min read
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:

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

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

Image 1

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


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
Question+5 Very Nice, but.... Pin
Kevin Marois17-Jul-19 8:39
professionalKevin Marois17-Jul-19 8:39 
QuestionWell done Pin
Dirk Bahle26-Nov-17 6:48
Dirk Bahle26-Nov-17 6:48 
Generalmy vote of 2 Pin
Mr.PoorEnglish14-Oct-17 11:44
Mr.PoorEnglish14-Oct-17 11:44 
PraiseWell done Pin
Graeme_Grant26-Sep-17 21:57
mvaGraeme_Grant26-Sep-17 21:57 
AnswerRe: Well done Pin
Clifford Nelson2-Oct-17 4:10
Clifford Nelson2-Oct-17 4:10 
QuestionGood job !!! Pin
Juan Francisco Morales Larios26-Sep-17 21:22
Juan Francisco Morales Larios26-Sep-17 21:22 
AnswerRe: Good job !!! Pin
Clifford Nelson2-Oct-17 4:10
Clifford Nelson2-Oct-17 4:10 

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.