15
votes

I had never noticed this before, but the WPF ListBox seems to change its SelectedItem when the Mouse is down, but has not yet been released. As a quick example, just create a simple ListBox with several ListBoxItems, like so:

<ListBox>
  <ListBoxItem>Hello</ListBoxItem>
  <ListBoxItem>World</ListBoxItem>
  <ListBoxItem>ListBox</ListBoxItem>
  <ListBoxItem>Test</ListBoxItem>
</ListBox>

fire up your application, press the mouse button (don't release it!) and move the mouse around. The SelectedItem will change as the mouse moves. This illustrates the larger problem (for me, at least), that a ListBox's SelectedItem will be set as soon as you mouse down, not when mouse up occurs. Usually that isn't a problem, but in my case I'd like to enable drag & drop on the items in my ListBox, without the items explicitly becoming selected.

I imagine my only recourse is to build a custom ItemsControl or Selector with selection-style semantics similar to ListBox, so really my question is more, why does ListBox work this way? Does anyone have any insight into this?

7

7 Answers

13
votes

I personally prefer MVVM and attached properties to tweak the behavior of elements.

Furthermore the solution proposed by Tomas Kosar doesn't seem to work when the ItemsSource property is bound.

Here's what I currently use (C# 7 syntax)

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

}

Now you can apply this to any ListBox (or Selector-derived class), e.g.

<ListBox ItemsSource="{Binding ViewModelItems}" 
    SelectedItem="{Binding SelectedViewModelItem}" 
    ui:SelectorBehavior.ShouldSelectItemOnMouseUp="True" />
8
votes

It might be a bit off topic but i just came up to similar problem. I do not want to do drag and drop but i want to select items on ListBox on MouseUp and not MouseDown. Although Sheena pseudo code might give some hint it still took me a while before i found out right solution. So this is my solution for my problem.

public class ListBoxSelectionItemChangedOnMouseUp : ListBox
{
    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
        {
            DependencyObject obj = this.ContainerFromElement((Visual)e.OriginalSource);
            if (obj != null)
            {
                FrameworkElement element = obj as FrameworkElement;
                if (element != null)
                {
                    ListBoxItem item = element as ListBoxItem;
                    if (item != null && this.Items.Contains(item))
                    {
                        this.SelectedItem = item;
                    }
                }
            }
        }
    }

    protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
    {
        e.Handled = true;
    }
}

I also wanted to select only on left mouse button. In case of drag and drop its necessary to save selected item in mouse down event and then use it in mouse up event. I hope this will help someone.

1
votes

I'm assuming you already tried to make a new mouse-down event that does what you want, and override the standard behavior that way... here's some pseudo-code that should do the trick:

ListBoxItem selected;
on_any_event_that_should_change_whats_selected()
{
    selected=whatever_you_want_selected;
}
on_selection_changed()
{
    theListBox.selectedItem=selected;
}

My wpf skillz are a little rusty but I think you'll need to store the item and then generate a container, so this pseudocode is a vast oversimplification but the algorithm should do the trick.

1
votes

An alternative approach that seems to be working for me:

private class SelectOnMouseUpListViewItem: ListViewItem
{
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        if (IsSelected)
            e.Handled = true;
        base.OnMouseLeftButtonDown(e);
    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        if (!IsSelected)
            base.OnMouseLeftButtonDown(e);
        base.OnMouseLeftButtonUp(e);
    }
}

protected override DependencyObject GetContainerForItemOverride() // in ListView
{
    return new SelectOnMouseUpListViewItem();
}
0
votes

I've found similar problem with ListView. I wasn't able to start drag-drop of some item, without loosing selection of another.

I solved it by deriving from ListView, and handling PreviewMouseDown event. Instead of this, I selected item on MouseUp.

Rest of drag logic is implemented using Reactive Extensions.

ListBox is similar to ListView, so you might be able to just derive from ListBox and it'll work.

Code:

public class DragDroppableListView : ListView
{
    private IDisposable _subscription;

    public DragDroppableListView()
    {
        Loaded += OnControlLoaded;
        Unloaded += OnControlUnloaded;
    }

    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        if (e.ChangedButton != MouseButton.Left) return;

        var obj = ContainerFromElement((Visual)e.OriginalSource);
        if (obj == null) return;

        var element = obj as FrameworkElement;
        if (element == null) return;

        var item = element as ListBoxItem;
        if (item == null) return;

        // select item
        item.IsSelected = true;
    }

    private void OnControlUnloaded(object sender, RoutedEventArgs e)
    {
        if (_subscription != null)
            _subscription.Dispose();
    }

    private void OnControlLoaded(object sender, RoutedEventArgs e)
    {
        var mouseDown = Observable.FromEventPattern<MouseButtonEventArgs>(this, "PreviewMouseDown");

        var mouseUp = Observable.FromEventPattern<MouseEventArgs>(this, "MouseUp");
        var mouseMove = Observable.FromEventPattern<MouseEventArgs>(this, "MouseMove");

        _subscription = mouseDown
            .Where(a => a.EventArgs.LeftButton == MouseButtonState.Pressed)
            .Where(o => !IsScrollBar(o.EventArgs))
            .Do(o => o.EventArgs.Handled = true)        // not allow listview select on mouse down
            .Select(ep => ep.EventArgs.GetPosition(this))
            .SelectMany(md => mouseMove
                .TakeWhile(ep => ep.EventArgs.LeftButton == MouseButtonState.Pressed)
                .Where(ep => IsMinimumDragSeed(md, ep.EventArgs.GetPosition(this)))
                .TakeUntil(mouseUp))
            .ObserveOnDispatcher()
            .Subscribe(_ => OnDrag());
    }

    private void OnDrag()
    {
        var item = GetItemUnderMouse();
        if (item == null) return;

        DragDrop.DoDragDrop(
            this,
            new DataObject(typeof(object), item),
            DragDropEffects.Copy | DragDropEffects.Move);
    }

    private ListViewItem GetItemUnderMouse()
    {
        return Items.Cast<object>()
            .Select(item => ItemContainerGenerator.ContainerFromItem(item))
            .OfType<ListViewItem>()
            .FirstOrDefault(lvi => lvi.IsMouseOver);
    }

    private static bool IsMinimumDragSeed(Point start, Point end)
    {
        return Math.Abs(end.X - start.X) >= SystemParameters.MinimumHorizontalDragDistance ||
               Math.Abs(end.Y - start.Y) >= SystemParameters.MinimumVerticalDragDistance;
    }

    private bool IsScrollBar(MouseEventArgs args)
    {
        var res = VisualTreeHelper.HitTest(this, args.GetPosition(this));
        if (res == null) return false;

        var depObj = res.VisualHit;
        while (depObj != null)
        {
            if (depObj is ScrollBar) return true;

            // VisualTreeHelper works with objects of type Visual or Visual3D.
            // If the current object is not derived from Visual or Visual3D,
            // then use the LogicalTreeHelper to find the parent element.
            if (depObj is Visual || depObj is System.Windows.Media.Media3D.Visual3D)
                depObj = VisualTreeHelper.GetParent(depObj);
            else
                depObj = LogicalTreeHelper.GetParent(depObj);
        }

        return false;
    }
}
0
votes

Using reflection is never ideal, but here is some code that works for me.

public class SelectOnMouseUpListBoxItem : ListBoxItem
{
    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        var received = _receivedMouseDown;
        _receivedMouseDown = null;

        // validate that the mouse left button down event was called on this list box item
        if (received != this)
            return;

        var parent = WpfUtility.FindVisualParent<SelectOnMouseUpListBox>(this);

        parent.NotifyListItemClickedImp(this, e.ChangedButton);

        base.OnMouseLeftButtonUp(e);
    }

    private SelectOnMouseUpListBoxItem _receivedMouseDown;

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        _receivedMouseDown = this;

        e.Handled = true;

        base.OnMouseLeftButtonDown(e);
    }
}

public class SelectOnMouseUpListBox : ListBox
{
    static SelectOnMouseUpListBox()
    {
        _notifyListItemClickedMethodInfo = typeof(ListBox).GetMethod("NotifyListItemClicked", BindingFlags.Instance | BindingFlags.NonPublic);

        if (_notifyListItemClickedMethodInfo == null)
            throw new NotSupportedException("Failed to get NotifyListItemClicked method info by reflection");
    }

    private static readonly MethodInfo _notifyListItemClickedMethodInfo;

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new SelectOnMouseUpListBoxItem();
    }

    public void NotifyListItemClickedImp(ListBoxItem item, MouseButton button)
    {
        _notifyListItemClickedMethodInfo.Invoke(this, new object[] {item, button});
    }
}

This retains the usual selection behaviour depending on the SelectionMode (Single, Multiple, Extended) of the ListBox.

0
votes

Tomas' answer is good, but may need to be tweaked in order for it to work. I had to do some casts and its not necessarily pretty, but it gets the job done.

This should be fairly straight forward to update to your needs.

private void MembersList_MouseUp(object sender, MouseButtonEventArgs e) {
    if (e.ChangedButton == MouseButton.Left) {
        DependencyObject obj = myListBox.ContainerFromElement((Visual)e.OriginalSource);
        if (obj != null) {
            FrameworkElement element = obj as FrameworkElement;
            if (element != null) {
                ListBoxItem item = element as ListBoxItem;
                if (item != null && ((MyViewModel)myListBox.DataContext).myListObject.Contains((MyListItem)item.DataContext)) {
                    myListBox.SelectedItem = item.DataContext;
                }
            }
        }
    }
}