Search code examples
c#wpflistviewdrag

How can I fire SelectionChanged event on MouseUp


I have a ListView, and I would like to change behaviour, so SelectionChanged event would fire on MouseUp, instead of MouseDown. The reason why I want to change behaviour, is because when I want to drag objects (on MouseMove), the selection is changed when I click (MouseDown) on the objects to move.

I found that piece of code, but this is working only for simple selection, and I would like to allow selecting several items.

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)
    {
        return (bool)element.GetValue(ShouldSelectItemOnMouseUpProperty);
    }

    private static void HandleShouldSelectItemOnMouseUpChange(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is Selector selector)
        {
            selector.PreviewMouseDown -= HandleSelectPreviewMouseDown;
            selector.MouseUp -= HandleSelectMouseUp;

            if (Equals(e.NewValue, true))
            {
                selector.PreviewMouseDown += HandleSelectPreviewMouseDown;
                selector.MouseUp += HandleSelectMouseUp;
            }
        }
    }

    private static void HandleSelectMouseUp(object sender, MouseButtonEventArgs e)
    {
        var selector = (Selector)sender;

        if (e.ChangedButton == MouseButton.Left && e.OriginalSource is Visual source)
        {
            var container = selector.ContainerFromElement(source);
            if (container != null)
            {
                var index = selector.ItemContainerGenerator.IndexFromContainer(container);
                if (index >= 0)
                {
                    selector.SelectedIndex = index;
                }
            }
        }
    }

    private static void HandleSelectPreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        e.Handled = e.ChangedButton == MouseButton.Left;
    }

    #endregion

}

Here is my Xaml :

<ListView x:Name="ListViewContract" SelectedItem="{Binding SelectedContract}"  ItemsSource="{Binding ListContractsView}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" MouseDoubleClick="ListViewContract_DoubleClick" GridViewColumnHeader.Click ="GridViewHeaderClicked" SelectionChanged="ListViewContract_SelectionChanged" Visibility="{Binding Grid1Visible, Converter={StaticResource BoolToVisConverter}}" 
    MouseMove="ListViewContract_MouseMove" local:SelectorBehavior.ShouldSelectItemOnMouseUp="True">
    <ListView.View>
        <GridView AllowsColumnReorder="true" x:Name="GridViewContract">
            <GridViewColumn DisplayMemberBinding="{Binding ID}" Header="ID" Width="{Binding WidthIDs, Mode=TwoWay}"/>
            <GridViewColumn DisplayMemberBinding="{Binding Name}" Header="{x:Static p:Resources.Name}" Width="{Binding WidthColumnContractName, Mode=TwoWay}"/>
            <GridViewColumn DisplayMemberBinding="{Binding Code}" Header="{x:Static p:Resources.Number}" Width="{Binding WidthColumnContractCode, Mode=TwoWay}"/>
            <GridViewColumn DisplayMemberBinding="{Binding Comm}" Header="{x:Static p:Resources.Commentary}" Width="{Binding WidthColumnContractComm, Mode=TwoWay}"/>
            <GridViewColumn DisplayMemberBinding="{Binding Revision}" Header="{x:Static p:Resources.Revision}" Width="{Binding WidthColumnContractRev, Mode=TwoWay}"/>
            <GridViewColumn Header="{x:Static p:Resources.Advancement}" Width="{Binding WidthColumnContractAdv, Mode=TwoWay}">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <Rectangle Name="AFF_Track"  Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgression}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractAss">
                                <Rectangle.ToolTip>
                                    <ContentControl  Template="{StaticResource ToolTipOperations}"/>
                                </Rectangle.ToolTip>
                            </Rectangle>
                            <Rectangle Name="AFF_Track2"  Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionWeight}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractAss">
                                <Rectangle.ToolTip>
                                    <ContentControl  Template="{StaticResource ToolTipOperations}"/>
                                </Rectangle.ToolTip>
                            </Rectangle>
                        </StackPanel>

                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>

            <GridViewColumn Header="{x:Static p:Resources.Advancement}" Width="{Binding WidthColumnContractAdvRep, Mode=TwoWay}">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <Rectangle Name="AFF_TrackRep"  Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionRep}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractRep">
                                <Rectangle.ToolTip>
                                    <ContentControl  Template="{StaticResource ToolTipOperations}"/>
                                </Rectangle.ToolTip>
                            </Rectangle>
                            <Rectangle Name="AFF_Track2Rep"  Height="12" Stroke="black" StrokeThickness="1" Fill="{Binding RectangleProgressionRepWeight}" Tag="{Binding ID}" MouseMove="mouseOverProgressionContractRep">
                                <Rectangle.ToolTip>
                                    <ContentControl  Template="{StaticResource ToolTipOperations}"/>
                                </Rectangle.ToolTip>
                            </Rectangle>
                        </StackPanel>

                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn DisplayMemberBinding="{Binding Manager.CompleteName}" Header="{x:Static p:Resources.ProjectManager}" Width="{Binding WidthColumnContractManager, Mode=TwoWay}"/>
            <GridViewColumn DisplayMemberBinding="{Binding Client.Name}" Header="{x:Static p:Resources.Customer}" Width="{Binding WidthColumnContractCustomer, Mode=TwoWay}"/>
            <GridViewColumn DisplayMemberBinding="{Binding Source}" Header="{x:Static p:Resources.Source}" Width="{Binding WidthColumnContractSource, Mode=TwoWay}"/>
        </GridView>
    </ListView.View>
</ListView>

Solution

  • You can modify your behavior to handle controls that extend MultiSelector (like the DataGrid) and ListBox individually. 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: random multi select while CTRL is pressed and range select while Shift key is pressed. Otherwise the Selector will behave as usual (single select on release of the left mouse button).

    SelectorService.cs

    public class SelectorService : DependencyObject
    {
      #region IsSelectItemOnMouseUpEnabled attached property
    
      public static readonly DependencyProperty IsSelectItemOnMouseUpEnabledProperty = DependencyProperty.RegisterAttached(
        "IsSelectItemOnMouseUpEnabled",
        typeof(bool),
        typeof(SelectorService),
        new PropertyMetadata(default(bool), OnIsSelectItemOnMouseUpEnabledChanged));
    
      public static void SetIsSelectItemOnMouseUpEnabled(DependencyObject attachedElement, bool value) => attachedElement.SetValue(IsSelectItemOnMouseUpEnabledProperty, value);
      public static bool GetIsSelectItemOnMouseUpEnabled(DependencyObject attachedElement) => (bool)attachedElement.GetValue(IsSelectItemOnMouseUpEnabledProperty);
    
      #endregion IsSelectItemOnMouseUpEnabled attached property
    
      #region SelectedItems attached property
    
      public static IList GetSelectedItems(DependencyObject attachedElement) => (IList)attachedElement.GetValue(SelectedItemsProperty);
      public static void SetSelectedItems(DependencyObject attachedElement, IList value) => attachedElement.SetValue(SelectedItemsPropertyKey, value);
    
      private static readonly DependencyPropertyKey SelectedItemsPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
        "SelectedItems",
        typeof(IList),
        typeof(SelectorService),
        new PropertyMetadata(default));
    
      public static readonly DependencyProperty SelectedItemsProperty = SelectedItemsPropertyKey.DependencyProperty;
    
      #endregion SelectedItems attached property
    
      #region SelectionMode attached property (private)
    
      private static SelectionMode GetOriginalSelectionModeBackup(DependencyObject attachedElement) => (SelectionMode)attachedElement.GetValue(OriginalSelectionModeBackupProperty);
      private static void SetOriginalSelectionModeBackup(DependencyObject attachedElement, SelectionMode value) => attachedElement.SetValue(OriginalSelectionModeBackupProperty, value);
    
      private static readonly DependencyProperty OriginalSelectionModeBackupProperty = DependencyProperty.RegisterAttached(
        "OriginalSelectionModeBackup",
        typeof(SelectionMode),
        typeof(SelectorService),
        new PropertyMetadata(default));
    
    
      #endregion SelectionMode attached property
    
      private static bool IsRandomMultiSelectEngaged
      => Keyboard.Modifiers is ModifierKeys.Control;
    
      private static bool IsRangeMultiSelectEngaged
        => Keyboard.Modifiers is ModifierKeys.Shift;
    
      private static Dictionary<Selector, bool> IsMultiSelectAppliedMap { get; } = new Dictionary<Selector, bool>();
    
      private static int SelectedRangeStartIndex { get; set; } = -1;
    
      private static int SelectedRangeEndIndex { get; set; } = -1;
    
      private static void OnIsSelectItemOnMouseUpEnabledChanged(DependencyObject attachedElement, DependencyPropertyChangedEventArgs e)
      {
        if (attachedElement 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;
    
          if (selector is ListBox listBox)
          {
            listBox.SelectionMode = GetOriginalSelectionModeBackup(listBox);
          }
        }
      }
    
      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)
        {
          ValidateListBoxSelectionMode(listBox);
          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);
      }
    
      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 (IsRandomMultiSelectEngaged)
        {
          MultiSelectItems(selector, itemContainerToSelect, currentSelectedItemContainer);
        }
        else if (IsRangeMultiSelectEngaged)
        {
          MultiSelectRangeOfItems(selector, itemContainerToSelect, currentSelectedItemContainer);
        }
        else
        {
          SingleSelectItem(selector, itemContainerToSelect, currentSelectedItemContainer);
        }
    
        IsMultiSelectAppliedMap[selector] = IsRandomMultiSelectEngaged || IsRangeMultiSelectEngaged;
      }
    
      private static void MultiSelectRangeOfItems(Selector selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
      {
        int clickedContainerIndex = selector.ItemContainerGenerator.IndexFromContainer(itemContainerToSelect);
    
        // In case there is not any preselected item. Otherwis SingleSlectItem() has already set the SelectedRangeStartIndex property.
        if (SelectedRangeStartIndex == -1)
        {
          SelectedRangeStartIndex = clickedContainerIndex;
          DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromIndex(SelectedRangeStartIndex);
          MultiSelectItems(selector, itemContainer, currentSelectedItemContainer);
          return;
        }
        // Complete the range selection
        else if (SelectedRangeEndIndex == -1)
        {
          bool isSelectionRangeFromTopToBotton = clickedContainerIndex > SelectedRangeStartIndex;
          if (isSelectionRangeFromTopToBotton)
          {
            SelectedRangeEndIndex = clickedContainerIndex;
          }
          else
          {
            // Selection is from bottom to top, so we need to swap start and end index
            // as they are used to initialize the for-loop.
            SelectedRangeEndIndex = SelectedRangeStartIndex;
            SelectedRangeStartIndex = clickedContainerIndex;
          }
    
          for (int itemIndex = SelectedRangeStartIndex; itemIndex <= SelectedRangeEndIndex; itemIndex++)
          {
            DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromIndex(itemIndex);
            bool isContainerUnselected = !Selector.GetIsSelected(itemContainer);
            if (isContainerUnselected)
            {
              MultiSelectItems(selector, itemContainer, currentSelectedItemContainer);
            }
          }
    
          // Only remember start index to append sequential ranges (more clicks while Shift key is pressed)
          // and invalidate the end index.
          SelectedRangeEndIndex = -1;
        }
      }
    
      private static void MultiSelectItems(Selector? selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
      {
        bool oldIsSelectedValue = Selector.GetIsSelected(itemContainerToSelect);
    
        // Toggle the current state
        bool newIsSelectedValue = oldIsSelectedValue ^= true;
    
        if (selector is ListBox listBox)
        {
          // In case the mode was overriden externally, force it back
          // but store the changed value to allow roll back when the behavior gets disabled.
          ValidateListBoxSelectionMode(listBox);
        }
    
        // If the current Selector instance does not support native multi select
        // we need to prevent the Selector from unselecting previous selected items.
        // By setting unselected items back to selected we can enforce a visual multi select feedback.
        if (selector is not MultiSelector and not ListBox)
        {
          if (newIsSelectedValue && currentSelectedItemContainer is not null)
          {
            Selector.AddUnselectedHandler(currentSelectedItemContainer, OnUnselected);
          }
        }
    
        Selector.SetIsSelected(itemContainerToSelect, newIsSelectedValue);
        (itemContainerToSelect as UIElement)?.Focus();
    
        if (selector is not MultiSelector and not ListBox)
        {
          object item = selector.ItemContainerGenerator.ItemFromContainer(itemContainerToSelect);
          IList selectedItems = GetSelectedItems(selector);
          if (newIsSelectedValue)
          {
            selectedItems.Add(item);
          }
          else
          {
            selectedItems.Remove(item);
          }
        }
      }
    
      private static void SingleSelectItem(Selector? selector, DependencyObject itemContainerToSelect, DependencyObject currentSelectedItemContainer)
      {
        bool isPreviousSelectMultiSelect = IsMultiSelectAppliedMap[selector];
    
        if (!isPreviousSelectMultiSelect)
        {
          // Unselect the currently selected
          if (currentSelectedItemContainer is not null)
          {
            Selector.SetIsSelected(currentSelectedItemContainer, false);
          }
        }
        // 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.
        else
        {
          // Invalidate tracked multi select range
          SelectedRangeStartIndex = -1;
          SelectedRangeEndIndex = -1;
          if (selector is ListBox listBox)
          {
            ValidateListBoxSelectionMode(listBox);
            listBox.UnselectAll();
          }
          else if (selector is MultiSelector multiSelector)
          {
            multiSelector.UnselectAll();
          }
          else
          {
            IList selectedItems = GetSelectedItems(selector);
            foreach (object item in selectedItems)
            {
              DependencyObject itemContainer = selector.ItemContainerGenerator.ContainerFromItem(item);
              Selector.SetIsSelected(itemContainer, false);
            }
    
            selectedItems.Clear();
          }
        }
    
        // Execute single selection
        Selector.SetIsSelected(itemContainerToSelect, true);
        (itemContainerToSelect as UIElement)?.Focus();
    
        if (selector is not MultiSelector and not ListBox)
        {
          IList selectedItems = GetSelectedItems(selector);
          selectedItems.Clear();
          selectedItems.Add(selector.SelectedItem);
        }
    
        // Track index in case the next click enabled select range (press Shift while click)
        int clickedContainerIndex = selector.ItemContainerGenerator.IndexFromContainer(itemContainerToSelect);
        SelectedRangeStartIndex = clickedContainerIndex;
    
        return;
      }
    
      private static void ValidateListBoxSelectionMode(ListBox listBox)
      {
        if (listBox.SelectionMode is not SelectionMode.Extended)
        {
          // In case the mode was overriden externally, force it back
          // but store the changed value to allow roll back when the behavior gets disabled.
          SetOriginalSelectionModeBackup(listBox, listBox.SelectionMode);
          listBox.SelectionMode = SelectionMode.Extended;
        }
      }
    
      private static void OnPreviewLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
        => e.Handled = true;
    }