WPF CheckedListBox SelectionMode="Multiple" not updating SelectedIndex on SelectionChanged Event

782 views Asked by At

I am testing Kelly Elias' article on Creating a WPF checkListBox. I'm needing to get the selectedIndex and the checkedbox text. Everything works as needed until I change the listbox's SelectionMode to "Multiple" which I need implemented. After that, the SelectedIndex nor the SelectedItem does not change using the SelectionChanged event. These two properties only show info of the first checkedbox. However, all of the checkedboxes are added to the SelectedItems collection. Can someone please assist with this issue?

Thank you in advance!!!

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace Jarloo
{
    public class Customer : INotifyPropertyChanged
    {
        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                NotifyPropertyChanged("Name");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class CheckedListItem<T> : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private bool isChecked;
        private T item;

        public CheckedListItem()
        {
        }

        public CheckedListItem(T item, bool isChecked = false)
        {
            this.item = item;
            this.isChecked = isChecked;
        }

        public T Item
        {
            get { return item; }
            set 
            {
                item = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
            }
        }

        public bool IsChecked
        {
            get { return isChecked; }
            set
            {
                isChecked = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
            }
        }
    }

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableCollection<CheckedListItem<Customer>> Customers { get; set; }

        public MainWindow()
        {
            InitializeComponent();
            Customers = new ObservableCollection<CheckedListItem<Customer>>();

            Customers.Add(new CheckedListItem<Customer>(new Customer() { Name = "Kelly Smith" }));
            Customers.Add(new CheckedListItem<Customer>(new Customer() { Name = "Joe Brown" }));
            Customers.Add(new CheckedListItem<Customer>(new Customer() { Name = "Herb Dean" }));
            Customers.Add(new CheckedListItem<Customer>(new Customer() { Name = "John Paul" }));

            DataContext = this;
        }

        private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            int index = listbox1.SelectedIndex;
            string testName = ((CheckedListItem<Customer>)listbox1.SelectedValue).Item.Name;
        }
    }
}


<Window x:Class="Jarloo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" WindowStartupLocation="CenterScreen">
    <Grid>
        <ListBox Name="listbox1" ItemsSource="{Binding Customers}" SelectionChanged="ListBox_SelectionChanged"
                 SelectionMode="Multiple">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}, Path=IsSelected}"
                              Content="{Binding Path=Item.Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>
1

There are 1 answers

0
OhBeWise On

The way the control works when SelectionMode is Multiple (or Extended), is to set the SelectedIndex only when the first item is selected (I.E. added to SelectedItems). Additional items get added to the end of the SelectedItems list, but SelectedIndex remains unchanged.

To trick this behavior into the desired results, we will take the newly added item(s) off the end of SelectedItems and essentially insert it(them) back into the front. But for this to work, we have to do more than just make the newest item the first item in SelectedItems. We have to empty the list and re-add each entry so that the default behavior now recognizes the desired item as the SelectedValue and will update the SelectedIndex. So we'll use a temporary list to help.

Additionally, we'll add a flag to indicate if we are currently busy correcting the SelectedItems order. This is needed since we will be modifying the list and ListBox.SelectionChanged will be recursively called.

private bool busy;

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  if (!this.busy)
  {
    this.busy = true;

    /*
     * If items were added to the end of the list,
     * reverse the list appropriately.
     * Removed items are self-handling.
     */
    if (e.AddedItems.Count > 0)
    {
      List<CheckedListItem<Customer>>  reversed = new List<CheckedListItem<Customer>>();

      // Reverse newly added items.
      foreach (var item in e.AddedItems)
      {
        listbox1.SelectedItems.Remove(item);
        reversed.Insert(0, (CheckedListItem<Customer>)item);
      }

      // Maintain previously reversed items' orders.
      foreach (var item in listbox1.SelectedItems)
      {
        reversed.Add((CheckedListItem<Customer>)item);
      }

      // Clear and reset selections to trigger SelectedIndex change.
      listbox1.UnselectAll();

      foreach (var item in reversed)
      {
        listbox1.SelectedItems.Add(item);
      }
    }

    int index = listbox1.SelectedIndex;
    string testName = listbox1.SelectedValue == null ? string.Empty : ((CheckedListItem<Customer>)listbox1.SelectedValue).Item.Name;
    System.Console.WriteLine("{0} {1}", index, testName);

    this.busy = false;
  }
}

Sidenote: Instead of the busy flag and condition check, you could instead do the following:

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  listbox1.SelectionChanged -= this.ListBox_SelectionChanged;

  // Same code within the condition...

  listbox1.SelectionChanged += this.ListBox_SelectionChanged;
}

It will achieve the same purpose of preventing a recursive call and subsequent stack overflow. I just don't know whether unsubscribing and subscribing the event handler is less or more expensive than the boolean check (my guess is more but researching, there seems to be no preference, as shown in these three results).