WPF Drag & drop from ListBox with SelectionMode Multiple
I've found a very simple way to enable Windows Explorer like drag/drop behaviour when having multiple items selected. The solution replaces the common ListBox
with a little derived shim that replaces the ListBoxItem
with a more intelligent version. This way, we can encapsulate the click state at the right level and call into the protected selection machinery of the ListBox
. Here is the relevant class. For a complete example, see my repo on github.
public class ListBoxEx : ListBox{ protected override DependencyObject GetContainerForItemOverride() { return new ListBoxItemEx(); } class ListBoxItemEx : ListBoxItem { private bool _deferSelection = false; protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { if (e.ClickCount == 1 && IsSelected) { // the user may start a drag by clicking into selected items // delay destroying the selection to the Up event _deferSelection = true; } else { base.OnMouseLeftButtonDown(e); } } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { if (_deferSelection) { try { base.OnMouseLeftButtonDown(e); } finally { _deferSelection = false; } } base.OnMouseLeftButtonUp(e); } protected override void OnMouseLeave(MouseEventArgs e) { // abort deferred Down _deferSelection = false; base.OnMouseLeave(e); } }}
So...having become the proud owner of a tumbleweed badge, I've got back on to this to try & find a way around it. ;-)
I'm not sure I like the solution so I'm still very much open to any better approaches.
Basically, what I ended up doing is remember what ListBoxItem was last clicked on & then make sure that gets added to the selected items before a drag. This also meant looking at how far the mouse moves before starting a drag - because clicking on a selected item to unselect it could sometimes result in it getting selected again if mouse bounce started a little drag operation.
Finally, I added some hot tracking to the listbox items so, if you mouse down on a selected item it'll get unselected but you still get some feedback to indicate that it will get included in the drag operation.
private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e){ var source = (FrameworkElement)sender; var hitItem = source.InputHitTest(e.GetPosition(source)) as FrameworkElement; hitListBoxItem = hitItem.FindVisualParent<ListBoxItem>(); origPos = e.GetPosition(null);}private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e){ hitListBoxItem = null;}private void HandleMouseMove(object sender, MouseEventArgs e){ if (ShouldStartDrag(e)) { hitListBoxItem.IsSelected = true; var sourceItems = (FrameworkElement)sender; var viewModel = (WindowViewModel)DataContext; DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move); hitListBoxItem = null; }}private bool ShouldStartDrag(MouseEventArgs e){ if (hitListBoxItem == null) return false; var curPos = e.GetPosition(null); return Math.Abs(curPos.Y-origPos.Y) > SystemParameters.MinimumVerticalDragDistance || Math.Abs(curPos.X-origPos.X) > SystemParameters.MinimumHorizontalDragDistance;}
XAML changes to include hot tracking...
<Style TargetType="ListBoxItem"> <Setter Property="Margin" Value="1"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <Grid> <Border Background="{TemplateBinding Background}" /> <Border Background="#BEFFFFFF" Margin="1"> <Grid> <Grid.RowDefinitions> <RowDefinition /><RowDefinition /> </Grid.RowDefinitions> <Border Margin="1" Grid.Row="0" Background="#57FFFFFF" /> </Grid> </Border> <ContentPresenter Margin="8,5" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="PowderBlue" /> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsMouseOver" Value="True" /> <Condition Property="IsSelected" Value="False"/> </MultiTrigger.Conditions> <Setter Property="Background" Value="#5FB0E0E6" /> </MultiTrigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter></Style>
One option would be not to allow ListBox or ListView to remove selected items until MouseLeftButtonUp is triggered.Sample code:
List<object> removedItems = new List<object>(); private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (e.RemovedItems.Count > 0) { ListBox box = sender as ListBox; if (removedItems.Contains(e.RemovedItems[0]) == false) { foreach (object item in e.RemovedItems) { box.SelectedItems.Add(item); removedItems.Add(item); } } } } private void ListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (removedItems.Count > 0) { ListBox box = sender as ListBox; foreach (object item in removedItems) { box.SelectedItems.Remove(item); } removedItems.Clear(); } }