Search code examples
c#eventslistviewmulti-select

C# ListView ItemSelectionChanged Event Multi Select get ONLY last item selected


Im using a multi-select ListView in C# .NET 4.5 The issue occurs when selecting multiple items (ie. Shift + End or Shift + Click, etc.) These are just a few examples of many different mouse/keyboard combinations for multi-selecting of course..

This is my event handler for when selecting items in a list:

private void lvTitles_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
    MessageBox.Show(e.Item.Text.ToString());
    //MessageBox just for testing I am actually running a SQL query here
}

My problem is that if I select 500 items the event is triggered 500 times. The intent is to get that last item the user selected (via keyboard/mouse combinations mentioned above), on and do something with it ... in my case I need to run a SQL query against it.

If I click first on item 0 in the listview it is ok to run the query, then when you shift+end it highlights all the rest, and I want it to run the query only on the last item selected. Instead it is running on every item in between.

EDIT: On another note, the event triggers as it unselects as well, in which case it really shouldn't do anything when deselecting.


Solution

  • Have you considered performing the action on a button press instead? That way they can also use Ctrl-Click to select any individual items they want?

    Otherwise what you would have to do is wait a certain amount of time before firing the action, known as debouncing, you can read a more about debouncing here: https://stackoverflow.com/a/4517995/984780

    I created a class you can use for debouncing:

    public class Debounce {
        private Action _action;
        private bool _isThreadRunning;
        private Thread _thread;
        private DateTime _runAt;
        private double _waitSeconds;
    
        private Debounce(double waitSeconds, Action action) {
            _action = action;
            _waitSeconds = waitSeconds;
        }
    
        private void Invoke() {
            _runAt = DateTime.Now.AddSeconds(_waitSeconds);
    
            lock(this) {
                if(!_isThreadRunning) {
                    _isThreadRunning = true;
    
                    _thread = new Thread(() => {
                        while(true) {
                            Thread.Sleep(100);
    
                            lock(this) {
                                if(DateTime.Now > _runAt) {
                                    _action();
                                    _isThreadRunning = false;
                                    _thread = null;
                                    break;
                                }
                            }
                        }
                    });
    
                    _thread.Start();
                }
            }
        }
    
        private static Dictionary<Action, Debounce> __debounces;
        private static Dictionary<Action, Debounce> _debounces {
            get {
                if(__debounces == null) {
                    __debounces = new Dictionary<Action, Debounce>();
                }
    
                return __debounces;
            }
        }
    
        public static void Run(double waitSeconds, Action action) {
            Debounce debounce;
    
            if(!_debounces.TryGetValue(action, out debounce)) {
                debounce = new Debounce(waitSeconds, action);
                _debounces.Add(action, debounce);
            }
    
            debounce._waitSeconds = waitSeconds;
            debounce.Invoke();
        }
    }
    

    Then you can change your code to this:

    private void lvTitles_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
    {
        Debounce.Run(5, () => MessageBox.Show(e.Item.Text.ToString()));
    }
    

    This should work no matter how they select items, it will run your code 5 seconds after their last selection action.

    I just wrote this class and did a quick test, a more thorough test would be advised. In any case hopefully it's enough to get the idea.