65.9K
CodeProject is changing. Read more.
Home

SelectedItems Behavior for ListBox and MultiSelector

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.64/5 (5 votes)

Oct 14, 2017

CPOL
viewsIcon

8800

downloadIcon

91

This is an alternative for "SelectedItems Behavior for ListBox and MultiSelector"

Introduction

I found the originals concept ( <- please follow the link ) good, but the implementation contains several great mistakes.

Issues of the Original

public class ViewModel{
   public string[] ItemsSource { get; } = new[] 
   { "Frist Item", "Second Item", "Third Item", "Fourth Item" };
   public ObservableCollection<string> SelectedItems { get; } = new ObservableCollection<string>();
}

On each access to this SelectedItems-Property, a new ObservableCollection will be created!
Binding several Controls to that, or access such Properties by Code will lead to complex mis-behavior.

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 d)        {
      return (IList)d.GetValue(SelectedItemsProperty);
   }
   //...

This is puzzling: Register a DependencyProperty as INotifyCollectionChanged, but the Property-Getter retrieves an IList-Object??
Moreover - what is it good for, to Register the INotifyCollectionChanged - Type?

In the following you see the CollectionChanged-Event subscribed, but step-wise debugging shows, that the code executes, but has no effect at all.

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 collection1)
         collection1.CollectionChanged -= CollectionChangedEventHandler;
   if (e.NewValue is INotifyCollectionChanged collection2)
         collection2.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);
}

There is no need, that the behavior consumes a .CollectionChanged - Event of anything (lines #2-6 + #16-20) - Moreover the IsBusy-Dependancy-Property (lines #25-26, #30, #33-42) has no effect too.


Proposal to fix it

public static class SelectedItemsBahavior {
   public static readonly DependencyProperty SelectedItemsProperty =
         DependencyProperty.RegisterAttached
         ("SelectedItems", typeof(IList), typeof(SelectedItemsBahavior),
         new PropertyMetadata(default(IList), OnAttach));
   public static void SetSelectedItems(DependencyObject d, IList value) {
      d.SetValue(SelectedItemsProperty, value);
   }
   public static IList GetSelectedItems(DependencyObject d) {
      return (IList)d.GetValue(SelectedItemsProperty);
   }
   private static void OnAttach(DependencyObject d, DependencyPropertyChangedEventArgs e) {
      var multiSelector = d as MultiSelector;
      if (multiSelector != null) {
         multiSelector.SelectionChanged += OnSelectionChanged;
         return;
      }
      var listBox = d as ListBox;
      if (listBox != null) {
         listBox.SelectionChanged += OnSelectionChanged;
         listBox.SelectionMode = SelectionMode.Multiple;
      }
   }
   private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
      var list = GetSelectedItems((DependencyObject)sender);
      foreach (var item in e.RemovedItems) list.Remove(item);
      foreach (var item in e.AddedItems) list.Add(item);
   }
}

Common implementation of an IList-DependencyProperty, and the behaviors job is simply to keep that lists content up to date.