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>
CodePudding user response:
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 (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;
}