c# – How can I fire SelectionChanged event on MouseUp

You can modify your behavior to handle controls that extend MultiSelector (like the DataGrid) and ListBox individual. ListBox (and therefore ListView too) is not a MultiSelector but supports multi selection when setting the ListBox.SelectionMode property to something different than SelectionMode.Single (which is the default).
For every other simple Selector you would have to track the selected items manually. You would also have to intercept the Selector.Unselected event to prevent the Selector from unselecting the selected items when in a multi select mode – Selector only supports single item selection.

The following example shows how you can track selected items in case the attached Selector is not a ListBox or a MultiSelector. For this reason the behavior exposes a public readonly SelectedItems dependency property.

The example also shows how to observe the pressed keyboard keys in order to filter multi select user actions. This example filters Shift or Ctrl keys as gesture to trigger the multi select behavior. Otherwise the Selector will behave as usual (single select on release of the left mouse button).

Note that the example does not implement range selection. Holding the Shift key while selecting items holding the same way down the Ctrl key. But this is simple to implement. Just select enclosed items of the range by index instead by item container.

public static class SelectorBehavior
{
  #region bool ShouldSelectItemOnMouseUp

  public static readonly DependencyProperty ShouldSelectItemOnMouseUpProperty = DependencyProperty.RegisterAttached(
    "ShouldSelectItemOnMouseUp",
    typeof(bool),
    typeof(SelectorBehavior),
    new PropertyMetadata(default(bool), HandleShouldSelectItemOnMouseUpChange));

  public static void SetShouldSelectItemOnMouseUp(DependencyObject element, bool value) => element.SetValue(ShouldSelectItemOnMouseUpProperty, value);
  public static bool GetShouldSelectItemOnMouseUp(DependencyObject element) => (bool)element.GetValue(ShouldSelectItemOnMouseUpProperty);

  #endregion

  public static IList GetSelectedItems(DependencyObject obj) => (IList)obj.GetValue(SelectedItemsProperty);
  public static void SetSelectedItems(DependencyObject obj, IList value) => obj.SetValue(SelectedItemsPropertyKey, value);

  private static readonly DependencyPropertyKey SelectedItemsPropertyKey =
      DependencyProperty.RegisterAttachedReadOnly("SelectedItems", typeof(IList), typeof(SelectorBehavior), new PropertyMetadata(default));

  public static readonly DependencyProperty SelectedItemsProperty = SelectedItemsPropertyKey.DependencyProperty;

  // Filter keyboard keys to enable multi select mode
  private static bool IsMultiSelectEngaged
    => Keyboard.Modifiers is ModifierKeys.Control
        or ModifierKeys.Shift;

  private static Dictionary<Selector, bool> IsMultiSelectAppliedMap { get; } = new Dictionary<Selector, bool>();

  private static void HandleShouldSelectItemOnMouseUpChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    if (d is not Selector selector)
    {
      return;
    }

    if ((bool)e.NewValue)
    {
      WeakEventManager<FrameworkElement, MouseButtonEventArgs>.AddHandler(selector, nameof(selector.PreviewMouseLeftButtonDown), OnPreviewLeftMouseButtonDown);
      WeakEventManager<FrameworkElement, MouseButtonEventArgs>.AddHandler(selector, nameof(selector.PreviewMouseLeftButtonUp), OnPreviewLeftMouseButtonUp);
      if (selector.IsLoaded)
      {
        InitializeAttachedElement(selector);
      }
      else
      {
        selector.Loaded += OnSelectorLoaded;
      }
    }
    else
    {
      WeakEventManager<FrameworkElement, MouseButtonEventArgs>.RemoveHandler(selector, nameof(selector.PreviewMouseLeftButtonDown), OnPreviewLeftMouseButtonDown);
      WeakEventManager<FrameworkElement, MouseButtonEventArgs>.RemoveHandler(selector, nameof(selector.PreviewMouseLeftButtonUp), OnPreviewLeftMouseButtonUp);

      SetSelectedItems(selector, null);
      IsMultiSelectAppliedMap.Remove(selector);
      selector.Loaded -= OnSelectorLoaded;
    }
  }

  private static void OnSelectorLoaded(object sender, RoutedEventArgs e)
  {
    var selector = sender as Selector;
    selector.Loaded -= OnSelectorLoaded;
    InitializeAttachedElement(selector);
  }

  private static void InitializeAttachedElement(Selector selector)
  {
    IList selectedItems = new List<object>();
    if (selector is ListBox listBox && listBox.SelectionMode != SelectionMode.Single)
    {
      selectedItems = listBox.SelectedItems;
    }
    else if (selector is MultiSelector multiSelector)
    {
      selectedItems = multiSelector.SelectedItems;
    }
    else
    {
      if (selector.SelectedItem is not null)
      {
        selectedItems.Add(selector.SelectedItem);
      }
    }

    SetSelectedItems(selector, selectedItems);
    IsMultiSelectAppliedMap.Add(selector, false);
  }

  // Override the unslect of the Selector 
  // to allow visual feedback of multiple selected items for every Selector control (even if it does not support it natively)
  private static void OnUnselected(object? sender, RoutedEventArgs e)
  {
    var itemContainer = sender as DependencyObject;
    Selector.SetIsSelected(itemContainer, true);
    Selector.RemoveUnselectedHandler(itemContainer, OnUnselected);
  }

  private static void OnPreviewLeftMouseButtonUp(object sender, MouseButtonEventArgs e)
  {
    var selector = sender as Selector;
    DependencyObject itemContainerToSelect = ItemsControl.ContainerFromElement(selector, e.OriginalSource as DependencyObject);
    DependencyObject currentSelectedItemContainer = selector.ItemContainerGenerator.ContainerFromItem(selector.SelectedItem);
    if (itemContainerToSelect is null)
    {
      return;
    }

    if (!IsMultiSelectEngaged)
    {
      SingleSelectItem(selector, itemContainerToSelect);
    }
    else
    {
      MultiSelectItems(sender, itemContainerToSelect, currentSelectedItemContainer);
    }

    IsMultiSelectAppliedMap[selector] = IsMultiSelectEngaged;
  }

  private static void MultiSelectItems(object sender, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
  {
    var selector = sender as Selector;
    bool isPreviousSelectMultiSelect = IsMultiSelectAppliedMap[selector];
    bool oldIsSelectedValue = Selector.GetIsSelected(itemContainerToSelect);

    // Toggle the current state
    bool newIsSelectedValue = oldIsSelectedValue ^= true;

    if (!isPreviousSelectMultiSelect && selector is ListBox listBox)
    {
      listBox.SelectionMode = SelectionMode.Extended;

      // Since the last operation was single select, we need to set the SelectedItems collection back to the ListBox.SelectedItems
      SetSelectedItems(listBox, listBox.SelectedItems);
    }

    if (selector is not MultiSelector and not ListBox)
    {
      if (newIsSelectedValue && currentSelectedItemContainer is not null)
      {
        Selector.AddUnselectedHandler(currentSelectedItemContainer, OnUnselected);
      }
    }

    Selector.SetIsSelected(itemContainerToSelect, newIsSelectedValue);

    if (selector is not MultiSelector and not ListBox)
    {
      object item = selector.ItemContainerGenerator.ItemFromContainer(itemContainerToSelect);
      if (newIsSelectedValue)
      {
        GetSelectedItems(selector).Add(item);
      }
      else
      {
        GetSelectedItems(selector).Remove(item);
      }
    }
  }

  private static void SingleSelectItem(Selector? selector, DependencyObject itemContainer)
  {
    bool isPreviousSelectMultiSelect = IsMultiSelectAppliedMap[selector];

    // If the Selector has multiple selected items and an item was clicked without the modifier key pressed,
    // then we need to switch back to single selection mode and only select the currently clicked item.
    if (isPreviousSelectMultiSelect)
    {
      IList selectedItems = selector is ListBox
        ? new List<object>()
        : GetSelectedItems(selector);

      if (selector is ListBox listBox)
      {
        listBox.UnselectAll();

        // We are not allowed to modify ListBoxSelectedItems in Single selection mode
        SetSelectedItems(listBox, selectedItems);

        listBox.SelectionMode = SelectionMode.Single;
      }
      else if (selector is MultiSelector multiSelector)
      {
        multiSelector.UnselectAll();
      }
      else
      {
        foreach (object item in GetSelectedItems(selector))
        {
          DependencyObject currentSelectedItemContainer = selector.ItemContainerGenerator.ContainerFromItem(item);
          Selector.SetIsSelected(currentSelectedItemContainer, false);
        }

        selectedItems.Clear();
      }
    }

    // Execute single selection
    Selector.SetIsSelected(itemContainer, true);
    if (selector is not MultiSelector)
    {
      GetSelectedItems(selector).Clear();
      GetSelectedItems(selector).Add(selector.SelectedItem);
    }

    return;
  }

  private static void OnPreviewLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
    => e.Handled = true;
}

Leave a Comment