Search code examples
c#wpfmvvmlistboxselectionchanged

Raise/Fire Listbox SelectionChangedEvent manually


I want to select programatically multiple items in my ListBox. So, to be as mvvm friendly as possible, I create a custom control inherited from ListBox. In this custom control I've made a dependency property allowing items selection changes. Here is the code of the OnPropertyChanged part :

private static void OnSetSelectionToPropertyChanged
            (DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            InitializableListBox list = d as InitializableListBox;
            Dictionary<int, string> toSelect = e.NewValue as Dictionary<int, string>;
            if (toSelect == null)
                return;

            list.SetSelectedItems(toSelect);
        }

The selection works great, but this solution does not raise the OnSelectionChanged event

So I try also :

private static void OnSetSelectionToPropertyChanged
            (DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            InitializableListBox list = d as InitializableListBox;
            Dictionary<int, string> toSelect = e.NewValue as Dictionary<int, string>;
            if (toSelect == null)
                return;

            SelectionChangedEventArgs e_selChanged;

            List<Object> removed = new List<object>();
            List<Object> added = new List<object>();

            //Clear the SelectedItems list
            while(list.SelectedItems.Count > 0)
            {
                removed.Add(list.SelectedItems[0]);
                list.SelectedItems.RemoveAt(0);
            }

            //Add each selected items
            foreach (var item in toSelect)
            {
                list.SelectedItems.Add(item);
                added.Add(list.SelectedItems[list.SelectedItems.Count - 1]);
            }

            //Raise the SelectionChanged event
            e_selChanged = new SelectionChangedEventArgs(SelectionChangedEvent,removed,added);
            list.OnSelectionChanged(e_selChanged);
        }

But this was not better. I think I'm not dealing the right way with the event so if you could help me, that would be great.

Thanks in advance.

EDIT

I have found another solution (more a hack actually) than @NETscape. I don't think it's better, but it seems to work pretty well, and it's maybe easier.

The trick is to made a dependency property wich allow you to access the SelectedItems property (wich is readonly and unbindable on the normal ListBox). Here is the code of my custom ListBox :

public class InitializableListBox : ListBox
    {

        public InitializableListBox()
        {
            SelectionChanged += CustomSelectionChanged;
        }

        private void CustomSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            InitializableListBox s = sender as InitializableListBox;
            if (s == null)
                return;

            s.CustomSelectedItems = s.SelectedItems;
        }

        #region CustomSelectedItems DependyProperty

        public static DependencyProperty CustomSelectedItemsProperty = 
            DependencyProperty.RegisterAttached("CustomSelectedItems",
            typeof(System.Collections.IList),typeof(InitializableListBox),
            new PropertyMetadata(null, OnCustomSelectedItemsPropertyChanged));

        public  System.Collections.IList  CustomSelectedItems 
        {
            get 
            {
                return (System.Collections.IList)GetValue(CustomSelectedItemsProperty); 
            }
            set 
            { 
                SetValue(CustomSelectedItemsProperty, value);                
            }
        }

        private static void OnCustomSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            InitializableListBox list = d as InitializableListBox;
            System.Collections.IList toSelect = e.NewValue as System.Collections.IList;
            if (toSelect == null)
                return;

            list.SetSelectedItems(toSelect);
        }

        #endregion
    }

Solution

  • See this

    If you set your ItemsSource to a collection, and the objects in the collection implement INPC, you can set the Style on ListViewItem to use the bound objects IsSelected property.

    See this answer to understand what I mean.

    Let's say you have ItemsSource="{Binding Items}", then you can do something like:

    Items.Where(item => item.IsSelected == true);  
    

    to return your list of items that are selected.
    You could also do Items[0].IsSelected = true; to programmatically select an item.

    In short, you shouldn't have to use a custom control to implement multiple selection on a ListView.

    EDIT

    I experienced the virtualization problem when I was implementing Telerik's RadGridView. I used a behavior to counter this problem and it seems to have worked:

    public class RadGridViewExt : Behavior<RadGridView>
    {
        private RadGridView Grid
        {
            get
            {
                return AssociatedObject as RadGridView;
            }
        }
    
        public INotifyCollectionChanged SelectedItems
        {
            get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }
    
        public static readonly DependencyProperty SelectedItemsProperty =
            DependencyProperty.Register("SelectedItems", typeof(INotifyCollectionChanged), typeof(RadGridViewExt), new PropertyMetadata(OnSelectedItemsPropertyChanged));
    
    
        private static void OnSelectedItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
        {
            var collection = args.NewValue as INotifyCollectionChanged;
            if (collection != null)
            {
                collection.CollectionChanged += ((RadGridViewExt)target).ContextSelectedItemsCollectionChanged;
            }
        }
    
        protected override void OnAttached()
        {
            base.OnAttached();
    
            Grid.SelectedItems.CollectionChanged += GridSelectedItemsCollectionChanged;
        }
    
        void ContextSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            UnsubscribeFromEvents();
    
            Transfer(SelectedItems as IList, AssociatedObject.SelectedItems);
    
            SubscribeToEvents();
        }
    
        void GridSelectedItemsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            UnsubscribeFromEvents();
    
            Transfer(AssociatedObject.SelectedItems, SelectedItems as IList);
    
            SubscribeToEvents();
        }
    
        private void SubscribeToEvents()
        {
            AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItemsCollectionChanged;
    
            if (SelectedItems != null)
            {
                SelectedItems.CollectionChanged += ContextSelectedItemsCollectionChanged;
            }
        }
    
        private void UnsubscribeFromEvents()
        {
            AssociatedObject.SelectedItems.CollectionChanged -= GridSelectedItemsCollectionChanged;
    
            if (SelectedItems != null)
            {
                SelectedItems.CollectionChanged -= ContextSelectedItemsCollectionChanged;
            }
        }
    
        public static void Transfer(IList source, IList target)
        {
            if (source == null || target == null)
                return;
    
            target.Clear();
    
            foreach (var o in source)
            {
                target.Add(o);
            }
        }
    }
    

    Inside RadGridView control:

    <i:Interaction.Behaviors>
        <local:RadGridViewExt SelectedItems="{Binding SelectedItems}" />
    </i:Interaction.Behaviors>
    

    where

    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    

    and remember to add reference to Interactivity assembly.